Инициализация в С++ действительно безумна. Лучше начинать с Си

в 14:51, , рубрики: braced-init-list, C, c++, copy-list-initialization, агрегированный тип, инициализация, конструктор, обучение программированию, разрешение перегрузки

Недавно мне напомнили, почему я считаю плохой идеей давать новичкам C++. Это плохая идея, потому что в C++ реальный бардак — хотя и красивый, но извращённый, трагический и удивительный бардак. Несмотря на нынешнее состояние сообщества, эта статья не направлена против современного C++. Скорее она частично продолжает статью Саймона Брэнда «Инициализация в C++ безумна», а частично — это послание каждому студенту, который хочет начать своё образование, глядя в бездну.

Типичные возражения студентов, когда им говорят об изучении C:

  • «Кто-то его ещё использует?»
  • «Это глупо»
  • «Почему мы изучаем C?»
  • «Мы должны учить что-то лучшее, например, C++» (смех в зале)


Кажется, многие студенты думают, что изучение C не имеет особого значения (от автора: это не так) и вместо этого нужно начинать с C++. Давайте рассмотрим только одну из причин, почему это абсурдное предложение: создание грёбаной переменной. В оригинальной статье Саймон Брэнд предположил, что читатель уже знаком со странностями инициализации в версиях до C++11. Мы же здесь посмотрим на некоторые из них и пойдём немного дальше.

Позвольте для начала пояснить, что в этой статье моё личное мнение, а не официальная позиция университета Дрекселя, где я преподаю на кафедре электротехники и вычислительной техники. Мои лекции обычно входят в курс инженерной программы, а не информатики, то есть больше относятся к системному программированию и встраиваемым системам.

Краткое содержание в одной гифке

u/AlexAlabuzhev на Reddit умудрился пересказать всю эту статью в одной гифке. (Думаю, это оригинальная работа Тимура Думлера)

Я ничего не имею против C++, но там много всего, что вам не нужно на раннем этапе.

Вот и всё. Иди домой. Погуляй с собакой. Постирай бельё. Позвони маме и скажи, что ты её любишь. Попробуй новый рецепт. Здесь нечего читать, ребята. В самом деле, подумайте о том, насколько плохо инженеры (то есть я) умеют доносить свои мысли…

Всё, я уговаривал как мог!

Итак, ты ещё здесь? Настоящий солдат. Если бы я мог, я бы дал тебе медаль! И вкусное шоколадное молочко!

Теперь вернёмся к нашему обычному… программированию.

Инициализация в C

Вступление

Сначала рассмотрим инициализацию в C, потому что она похожа на C++ по соображениям совместимости. Это будет довольно быстро, потому что C такой скучный и простой (кхм). Эту инициализацию назубок заучивает каждый новичок, потому что в C она работает иначе, чем во многих новых статически типизированных языках. Там либо инициализация по умолчанию для приемлемых значений, либо выдаётся ошибка компиляции.

int main() {
    int i;
    printf("%d", i);
}

Любой нормальный программист на C знает, что это инициализирует i как неопределённое значение (для всех намерений и целей i не инициализирована). Обычно рекомендуется инициализировать переменные, когда они определены, например int i = 0;, и переменные всегда следует инициализировать перед использованием. Независимо от того, сколько раз повторять, кричать, орать мягко напоминать студентам об этом, остаются те, кто считает, что переменная по умолчанию инициализируется в 0.

Отлично, попробуем ещё один простой пример.

int i;

int main() {
    printf("%d", i);
}

Очевидно, это одно и то же? Мы понятия не имеем о значении i — она может быть любой.

Нет.

Поскольку у переменной есть статическая продолжительность хранения, она инициализируется в беззнаковый ноль. Вы спросите, почему? Потому что так сказано в стандарте. Аналогичное поведение у типов указателей, которые я даже не собираюсь рассматривать в этой статье.

Окей, посмотрим на структуры.

struct A {
    int i;
};

int main() {
    struct A a;
    printf("%d", a.i);
}

То же самое. a не инициализирована. Мы увидим предупреждение при компиляции.

$ gcc -Wuninitalized a.c
a.c: In function ‘main’:
a.c:9:5: warning: ‘a.i’ is used uninitialized in this function [-Wuninitialized]
     printf("%dn", a.i);

В C можно инициализировать объект несколькими простыми способами. Например: 1) с помощью вспомогательной функции, 2) во время определения или 3) присвоить некое глобальное значение по умолчанию.

struct A {
    int i;
} const default_A = {0};

void init_A(struct A *ptr) {
    ptr->i = 0;
}

int main() {
    /* helper function */
    struct A a1;
    init_A(&a1);

    /* during definition;
     * Initialize each member, in order. 
     * Any other uninitialized members are implicitly
     * initialized as if they had static storage duration. */
    struct A a2 = {0};

    /* Error! (Well, technically) Initializer lists are 'non-empty' */
    /* struct A a3 = {}; */

    /* ...or use designated initializers if C99 or later */
    struct A a4 = {.i = 0};

    /* default value */
    struct A a5 = default_A;
}

Это практически всё, что нужно знать об инициализации в C, и этого достаточно, чтобы вызвать множество хитрых ошибок во многих студенческих проектах. И уж точно проблемы появятся, если считать, что по умолчанию всё инициализируется в 0.

Инициализация в C++

Акт 1. Наш герой начинает путь

Если вам не терпится узнать все ужасы чудеса C++, сначала изучите способы инициализации переменных. Здесь такое же поведение, как в C из предыдущего кода, но с некоторыми оговорками в правилах этого поведения. В тексте я буду выделять курсивом специфический жаргон C++, чтобы подчеркнуть те моменты, где я не просто произвольно называю вещи, а указывают на огромное количество новых… возможностей… в C++ по сравнению с C. Начнём с простого:

struct A {
    int i;
};

int main() {
    A a;
    std::cout << a.i << std::endl;
}

Здесь у С и C++ почти одинаковое поведение. В C просто создаётся объект типа A, значение которого может быть любым. В C++ a инициализирована по умолчанию, то есть для построения структуры используется конструктор по умолчанию. Поскольку A настолько тривиальна, у неё неявно определённый конструктор по умолчанию, который в этом случае ничего не делает. Неявно определенный конструктор по умолчанию «имеет точно такой же эффект», как:

struct A {
    A(){}
    int i;
}

Чтобы проверить наличие неинициализированного значения, смотрим на предупреждение во время компиляции. На момент написания этой статьи g++ 8.2.1 выдавал хорошие предупреждения, а clang++ 7.0.1 в этом случае ничего не выдавал (с установленным -Wuninitialized). Обратите внимание, что включена оптимизация для просмотра дополнительных примеров.

$ g++ -Wuninitalized -O2 a.cpp
a.cpp: In function ‘int main()’:
a.cpp:9:20: warning: ‘a.A::i’ is used uninitialized in this function [-Wuninitialized]
     std::cout << a.i << std::endl;

По сути именно этого мы ожидаем от C. Так как же инициализировать A::i?

Акт 2. Наш герой спотыкается

Наверное, можно применить те же способы, что и в С? В конце концов, C++ является надмножеством С, верно? (кхм)

struct A {
    int i;
};

int main() {
    A a = {.i = 0};
    std::cout << a.i << std::endl;
}

$ g++ -Wuninitialized -O2 -pedantic-errors a.cpp
a.cpp: In function ‘int main()’:
a.cpp:9:12: error: C++ designated initializers only available with -std=c++2a or -std=gnu++2a [-Wpedantic]
     A a = {.i = 0};

Вот вам и родственники. Явные инициализаторы не поддерживаются в C++ до C++20. Это стандарт C++, который планируется к выходу в 2020 году. Да, в C++ функцию реализуют через 21 год после того, как она появилась C. Обратите внимание, что я добавил -pedantic-errors для удаления поддержки нестандартных расширений gcc.

Что насчёт такого?

struct A {
    int i;
};

int main() {
    A a = {0};
    std::cout << a.i << std::endl;
}

$ g++ -Wuninitialized -O2 -pedantic-errors a.cpp
$

Ну хоть это работает. Мы также можем сделать A a = {}; с тем же эффектом, что и нулевая инициализация a.i. Это потому что A представляет собой агрегированный тип. Что это такое?

До C++11 агрегированный тип (по сути) является либо простым массивом в стиле C, либо структурой, которая выглядит как простая структура C. Ни спецификаторов доступа, ни базовых классов, ни пользовательских конструкторов, ни виртуальных функций. Агрегированный тип получает агрегированную инициализацию. Что это значит?

  1. Каждый объект класса инициализируется каждым элементом связного списка по порядку.
  2. Каждый объект без соответствующего связного списка элементов получит значение «инициализировано».

Отлично, что это значит? Если у объекта другой тип класса с пользовательским конструктором, будет вызван этот конструктор. Если объект является типом класса без пользовательского конструктора, как A, он будет рекурсивно инициализирован определённым значением. Если у нас встроенный объект, как int i, то он инициализируется нулём.

Урррррррааа! Наконец-то мы получили своего рода значение по умолчанию: ноль! Ух ты.

После C++11 ситуация выглядит иначе… вернёмся к этому позже.

Трудно запомнить и запутано? Обратите внимание, что у каждой версии C++ свой набор правил. Так и есть. Это чертовски запутано и никому не нравится. Эти правила обычно действуют, поэтому обычно система работает так, будто вы инициализируете элементы как ноль. Но на практике лучше явно всё инициализировать. Я не придираюсь к агрегированной инициализации, но мне не нравится необходимость пробираться сквозь дебри стандарта, чтобы точно узнать, что происходит во время инициализации.

Акт 3. Герой забрёл в пещеру

Что ж, инициализируем А методом C++ с конструкторами (торжественная музыка)! Можем назначить элементу i в структуре А начальное значение в пользовательском конструкторе по умолчанию:

struct A {
    A() : i(0) {}
    int i;
};

Это инициализирует i в списке инициализаторов членов. Более грязный способ — установить значение внутри тела конструктора:

struct A {
    A() { i = 0; }
    int i;
};

Поскольку тело конструктора может делать практически что угодно, лучше выделить инициализацию в список инициализаторов членов (технически часть тела конструктора).

В C++11 и более поздних версиях можно использовать дефолтные инициализаторы членов (серьёзно, по возможности просто используйте их).

struct A {
    int i = 0; // default member initializer, available in C++11 and later
};

Окей, теперь конструктор по умолчанию гарантирует, что i установлен в 0, когда любая структура A инициализируется по умолчанию. Наконец, если мы хотим разрешить пользователям A задать начальное значение i, можно для этого создать другой конструктор. Или смешать их вместе с аргументами по умолчанию:

struct A {
    A(int i = 0) : i(i) {}
    int i;
};

int main() {
    A a1;
    A a2(1);

    std::cout << a1.i << " " << a2.i << std::endl;
}

$ g++ -pedantic-errors -Wuninitialized -O2 a.cpp
$ ./a.out
0 1

Примечание. Нельзя написать A a(); для вызова конструктора по умолчанию, потому что он будет воспринят как объявление функции с именем a, которая не принимает аргументов и возвращает объект A. Почему? Потому что кто-то когда-то давно хотел разрешить объявления функций в блоках составных операторов, и теперь мы с этим застряли.

Отлично! Вот и всё. Миссия выполнена. Вы получили толчок и готовы продолжать приключения в мире C++, раздобыв полезное руководство по выживанию с инструкциями по инициализации переменных. Разворачиваемся и идём дальше!

Акт 4. Герой продолжает погружаться в темноту

Мы могли бы остановиться. Но, если мы хотим использовать современные возможности современного C++, то должны углубиться дальше. На самом деле моя версия g++ (8.2.1), по умолчанию использует gnu++1y, что эквивалентно C++14 с некоторыми дополнительными расширениями GNU. Более того, эта версия g++ также полностью поддерживает C++17. «Разве это имеет значение?» — можете вы спросить. Парень, надевай свои рыболовные сапоги и следуй за мной в самую гущу.

Во всех последних версиях, включая C++11, реализован этот новомодный способ инициализации объектов, который называется список инициализации. Чувствуете, как холодок пробежал по спине? Это также называется единообразной инициализацией. Есть несколько веских причин использовать этот синтаксис: см. здесь и здесь. Одна забавная цитата из FAQ:

Единообразная инициализация C++11 не является абсолютно единообразной, но это почти так.

Список инициализации применяется с фигурными скобками ({thing1, thing2, ...}, это называется braced-init-list) и выглядит следующим образом:

#include <iostream> 
struct A {
    int i;
};
int main() {
    A a1;      // default initialization -- as before
    A a2{};    // direct-list-initialization with empty list
    A a3 = {}; // copy-list-initialization with empty list
    std::cout << a1.i << " " << a2.i << " " << a3.i << std::endl;
}

$ g++ -std=c++11 -pedantic-errors -Wuninitialized -O2 a.cpp
a.cpp: In function ‘int main()’:
a.cpp:9:26: warning: ‘a1.A::i’ is used uninitialized in this function [-Wuninitialized]
     std::cout << a1.i << " " << a2.i << " " << a3.i « std::endl;

Эй, эй, вы это заметили? Остался неинициализированным только a1.i. Очевидно, что список инициализации работает иначе, чем просто вызов конструктора.

A a{}; производит то же поведение, что и A a = {};. В обоих случаях a инициализируется пустым списком braced-init-list. Кроме того, A a = {}; больше не называется агрегатной инициализацией — теперь это copy-list-initialization (вздыхает). Мы уже говорили, что A a; создаёт объект с неопределённым значением и вызывает конструктор по умолчанию.

В строках 7/8 происходит следующее (помните, что это после C++11):

  1. Список инициализации для A приводит ко второму пункту.
  2. Срабатывает агрегатная инициализация, поскольку A является агрегатным типом.
  3. Поскольку список пуст, все члены инициализируются пустыми списками.
    1. int i{} приводит к инициализации значения i, равного 0.

А если список не пуст?

int main() {
    A a1{0}; 
    A a2{{}};
    A a3{a1};
    std::cout << a1.i << " " << a2.i << " " << a3.i << std::endl;
}

$ g++ -std=c++11 -pedantic-errors -Wuninitialized -O2 a.cpp
$

a1.i инициализируется в 0, a2.i инициализируется пустым списком, а a3 — копия, построенная из a1. Вы ведь знаете, что такое конструктор копий, верно? Тогда вы знаете также о конструкторах перемещения, ссылках rvalue, а также передаваемых ссылках, pr-значениях, x-значениях, gl-значе… ладно, неважно.

К сожалению, в каждой версии с C++11 значение агрегата изменялось, хотя функционально до сих пор между агрегатами C++17 и C++20 нет никакой разницы. В зависимости от того, какая используется версия стандарта C++, что-то может быть или не быть агрегатом. Тренд в направлении либерализации. Например, публичные базовые классы в агрегатах разрешены начиная с C++17, что в свою очередь усложняет правила инициализации агрегатов. Всё замечательно!

Как себя чувствуете? Немного водички? Сжимаются кулаки? Может, сделаем перерыв, выйдем на улицу?

Акт 5. Прощай, здравый смысл

Что произойдет, если A не является агрегатным типом?

Вкратце, что такое агрегат:

  • массив или
  • структура/класс/объединение, где
    • нет приватных/защищённых членов
    • нет заявленных или предоставленных пользователем конструкторов
    • нет виртуальных функций
    • нет инициализаторов членов по умолчанию (в C++11, для последующих версий без разницы)
    • нет базовых классов (публичные базовые классы разрешены в C++17)
    • нет унаследованных конструкторов (using Base::Base;, в C++17)

Так что неагрегатный объект может быть таким:

#include <iostream>
struct A {
    A(){};
    int i;
};
int main() {
    A a{};
    std::cout << a.i << std::endl;
}

$ g++ -std=c++11 -pedantic-errors -Wuninitialized -O2 a.cpp
a.cpp: In function ‘int main()’:
a.cpp:8:20: warning: ‘a.A::i’ is used uninitialized in this function [-Wuninitialized]
     std::cout << a.i << std::endl;

Здесь у A есть предоставленный пользователем конструктор, поэтому инициализация списка работает иначе.

В строке 7 происходит следующее:

  1. Список инициализации для A приводит ко второму пункту.
  2. Не-агрегат с пустым braced-init-list вызывает инициализацию значения, идём к третьему пункту.
  3. Найден пользовательский конструктор, так что вызывается конструктор по умолчанию, который ничего не делает в этом случае, a.i не инициализируется.

Что такое конструктор, предоставленный пользователем?

struct A {
    A() = default;
};

Это не конструктор, предоставленный пользователем. Это как если вооще не объявлено никакого конструктора, а A является агрегатом.

struct A {
    A();
};
A::A() = default;

Вот это конструктор, предоставленный пользователем. Это словно мы написали A(){} в теле, где А не является агрегатом.

И угадайте что? В C++20 формулировка изменилась: теперь она требует, чтобы у агрегатов не было объявленных пользователем конструкторов :). Что это означает на практике? Я не уверен! Давайте продолжим.

Как насчет следующего:

#include <iostream>
class A {
    int i;
    friend int main();
};
int main() {
    A a{};
    std::cout << a.i << std::endl;
}

A — это класс, а не структура, поэтому i будет приватным, и нам пришлось установить main в качестве дружественной функции. Что делает А не агрегатом. Это просто обычный тип класса. Это значит, что a.i останется неинициализированным, верно?

$ g++ -std=c++11 -pedantic-errors -Wuninitialized -O2 a.cpp
$

Чёрт побери. И это тогда, когда мы вроде начали разбираться со всем этим. Оказывается, a.i инициализируется как 0, даже если не вызывает инициализацию агрегата:

  1. Инициализация списка для A, переходим к пункту 2.
  2. Неагрегат, тип класса с конструктором по умолчанию, и пустой список braced-init-list вызывают инициализацию значения, переходим к пункту 3.
  3. Предоставленный пользователем конструктор не найден, поэтому инициализируем объект как ноль, переходим к пункту 4.
  4. Вызов инициализации по умолчанию, если неявно определённый конструктор по умолчанию не тривиален (в данном случае условие не срабатывает и ничего не происходит).

Что если мы попробуем агрегатную инициализацию:

#include <iostream>
class A {
    int i;
    friend int main();
};
int main() {
    A a = {1};
    std::cout << a.i << std::endl;
}

$ g++ -std=c++11 -pedantic-errors -Wuninitialized -O2 a.cpp
a.cpp: In function ‘int main()’:
a.cpp:7:13: error: could not convert ‘{1}’ from ‘<brace-enclosed initializer list>’ to ‘A’
     A a = {1};

A не является агрегатом, поэтому происходит следующее:

  1. Инициализация списка для A, переходим к пункту 2.
  2. Поиск подходящего конструктора.
  3. Нет способа преобразовать 1 в A, компиляция завершается ошибкой.

В качестве бонуса озорной примерчик:

#include <iostream>
struct A {
    A(int i) : i(i) {}
    A() = default;
    int i;
};
int main() {
    A a{};
    std::cout << a.i << std::endl;
}

Здесь нет приватных переменных, как в предыдущем примере, но есть пользовательский конструктор, как в предпоследнем примере: таким образом, A не является агрегатом. Предоставленный пользователем конструктор исключает нулевую инициализацию, верно?

$ g++ -std=c++11 -pedantic-errors -Wuninitialized -O2 a.cpp
$

Нет! Разберёмся по пунктам:

  1. Инициализация списка для A, переходим к пункту 2.
  2. Неагрегат, тип класса с конструктором по умолчанию, и пустой список braced-init-list вызывают инициализацию значения, переходим к пункту 3.
  3. Не найден пользовательский конструктор по умолчанию (вот что я упустил выше), поэтому объект инициализируется как ноль, переходим к пункту 4.
  4. Вызов инициализации по умолчанию, если неявно определённый конструктор по умолчанию не тривиален (в данном случае условие не срабатывает и ничего не происходит).

Один последний пример:

#include <iostream>
struct A {
    A(){}
    int i;
};
struct B : public A {
    int j;
};
int main() {
    B b = {};
    std::cout << b.i << " " << b.j << std::endl;
}

$ g++ -std=c++11 -pedantic-errors -Wuninitialized -O2 a.cpp
a.cpp: In function ‘int main()’:
a.cpp:11:25: warning: ‘b.B::<anonymous>.A::i’ is used uninitialized in this function [-Wuninitialized]
     std::cout << b.i << " " << b.j << std::endl;

b.j инициализируется, а b.i нет. Что происходит в этом примере? Не знаю! Все базы b и члены здесь должны получить нулевую инициализацию. Я задал вопрос на Stack Overflow, и на момент публикации этого сообщения не получил твёрдого ответа, кроме возможной ошибки компилятора люди пришли к консенсусу, что здесь ошибка компилятора. Эти правила тонкие и сложные для всех. Для сравнения, статический анализатор clang (не обычный компилятор) вообще не предупреждает о неинициализированных значениях. Разбирайтесь сами.

...(тупо смотрит на вас) (взгляд превращается в вежливую улыбку) хорошо, давайте нырнём ещё глубже!

Акт 6. Бездна

В C++11 появилось нечто под названием std::initializer_list. У него собственный тип: очевидно, std::initializer_list<T>. Вы можете создать его с помощью braced-init-list. И кстати, braced-init-list для списка инициализации не имеет типа. Не путайте initializer_list со списком инициализации и braced-init-list! Все они имеют отношение к спискам инициализаторов членов и инициализаторам членов по умолчанию, так как помогают инициализировать нестатические элементы данных, но при этом сильно отличаются. Они связаны, но разные! Несложно, правда?

struct A {
    template <typename T>
    A(std::initializer_list<T>) {}
    int i;
};

int main() {
    A a1{0};
    A a2{1, 2, 3};
    A a3{"hey", "thanks", "for", "reading!"};
    std::cout << a1.i << a2.i << a3.i << std::endl;
}

$ g++ -std=c++17 -pedantic-errors -Wuninitialized -O2 a.cpp
a.cpp: In function ‘int main()’:
a.cpp:12:21: warning: ‘a1.A::i’ is used uninitialized in this function [-Wuninitialized]
     std::cout << a1.i << a2.i << a3.i << std::endl;
                     ^
a.cpp:12:29: warning: ‘a2.A::i’ is used uninitialized in this function [-Wuninitialized]
     std::cout << a1.i << a2.i << a3.i << std::endl;
                             ^
a.cpp:12:37: warning: ‘a3.A::i’ is used uninitialized in this function [-Wuninitialized]
     std::cout << a1.i << a2.i << a3.i << std::endl;

Окей. У A один шаблонный конструктор, который принимает std::initializer_list<T>. Каждый раз вызывается конструктор, предоставляемый пользователем, что ничего не делает, поэтому i остаётся неинициализированным. Тип T выводится в зависимости от элементов в списке, а новый конструктор создаётся в зависимости от типа.

  • Таким образом, в восьмой строке {0} выводится как std::initializer_list<int> с одним элементом 0.
  • В девятой строке {1, 2, 3} выводится как std::initializer_list<int> с тремя элементами.
  • В десятой строке список инициализации braced-init-list выводится как std::initializer_list<const char*> с четырьмя элементами.

Примечание: A a{} приведёт к ошибке, так как тип не может быть выведен. Например, нам нужно написать a{std::initializer_list<int> {}}. Или мы можем точно указать конструктор, как в A(std::initializer_list<int>){}.

std::initializer_list действует примерно как типичный контейнер STL, но только с тремя компонентными функциями: size, begin и end. Итераторы begin и end вы можете нормально разыменовать, увеличивать и сравнивать. Это полезно, когда требуется инициализировать объект списками разной длины:

#include <vector>
#include <string>
int main() {
    std::vector<int> v_1_int{5};
    std::vector<int> v_5_ints(5);
    std::vector<std::string> v_strs = {"neato!", "blammo!", "whammo!", "egh"};
}

У std::vector<T> есть конструктор, который принимает std::initializer_list<T>, поэтому мы можем легко инициализировать векторы, как показано выше.

Примечание. Вектор v_1_int создан из его конструктора, который берёт std::initializer_list<int< init с одним элементом 5.

Вектор v_5_ints создан из конструктора size_t count, который инициализирует вектор из (5) элементов и инициализирует их в значения (в данном случае все равны 0).

Оки–доки, последний пример:

#include <iostream>
struct A {
    A(std::initializer_list<int> l) : i(2) {}
    A(int i = 1) : i(i) {}
    int i;
};
int main() {
    A a1;
    A a2{};
    A a3(3);
    A a4 = {5};
    A a5{4, 3, 2};
    std::cout << a1.i << " "
              << a2.i << " "
              << a3.i << " "
              << a4.i << " "
              << a5.i << std::endl;
}

На первый взгляд, это не слишком сложно. У нас два конструктора: один принимает std::initializer_list<int>, а другой с аргументами по умолчанию принимает int. Прежде чем посмотреть на выдачу ниже, попробуйте сказать, каким будет значение для каждого i.

Подумали...? Посмотрим, что получится.

$ g++ -std=c++11 -pedantic-errors -Wuninitialized -O2 a.cpp
$ ./a.out
1 1 3 2 2

С a1 всё должно быть легко. Это простая инициализация по умолчанию, которая выбирает конструктор по умолчанию, используя его аргументы по умолчанию. a2 использует список инициализации с пустым списком. Поскольку у A есть конструктор по умолчанию (с аргументами по умолчанию), происходит инициализация значения с простым обращением к этому конструктору. Если бы у A не было этого конструктора, то пошло бы обращение к конструктору в третьей строке с вызовом пустого списка. a3 использует скобки, а не список braced-init-list, поэтому разрешение перегрузки выбирает 3 с конструктором, принимающим int. Далее, а4 использует список инициализации, для которого разрешение перегрузки склоняется в пользу конструктора, принимающего объект std::initializer_list. Очевидно, a5 нельзя соотнести с каким-то int, поэтому используется тот же конструктор, что и для a4.

Эпилог

Надеюсь, вы поняли, что эта статья (в основном) полемическая и, надеюсь, немного информативная. Многие описанные здесь нюансы можно игнорировать, и язык будет предсказуемо реагировать, если вы не забудете инициализировать переменные перед использованием и инициализировать элементы данных во время построения. Для написания грамотного кода необязательно изучать все пограничные ситуации С++, вы всё равно по ходу работы разберётесь с подводными камнями и идиомами. Для ясности, список инициализация — хорошая вещь. Если вы написали конструктор по умолчанию, он вызывается и должен всё инициализировать. В противном случае все инициализируется нулём, а затем независимо активируются дефолтные инициализаторы членов. Неинициализированное поведение тоже нужно оставить, потому что где-то, вероятно, есть код, который полагается на неинициализированные переменные.

Надеюсь, мне удалось продемонстрировать, что C++ большой, трудный язык (по многим историческим причинам). Вся статья посвящена нюансам инициализации. Просто инициализации переменных. И мы даже не раскрыли тему целиком, а кратко описали лишь 5 типов инициализации. Саймон в оригинальной статье упоминает 18 типов инициализации.

Я бы не хотел обучать новичков программированию на примере C++. В этой статье не нашлось места концепциям системного программирования, рассуждениям о парадигмах программирования, методологиям решения задач или фундаментальным алгоритмам. Если вы заинтересованы в C++, то записывайтесь на курс конкретно по C++, но имейте в виду, что там будут изучать именно этот язык. Если вам интересует C с классами или C с пространствами имён, то сначала узнайте о реализации this и коллизиях идентификаторов в C.

C — отличный, чёткий, быстрый, хорошо поддерживаемый и широко используемый язык для решения проблем в различных областях. И у него точно нет 18 типов инициализации.


Кстати, я совершенно забыл, что рассуждал точно на эту тему месяц назад. Вот что делает подсознание.


Обсуждение этой статьи и критика на разных форумах:

  1. Lobste.rs
  2. Hacker News
  3. Reddit

Отвечая на самую распространённую критику: да, можно научиться разумным способам инициализации переменных и никогда не встретиться с бездной. На этот счёт я специально написал в эпилоге, что список инициализации — хорошая вещь. Лично я редко пользуюсь шаблонами, но всё равно использую C++. Дело не в этом. Дело в том, что начинающий программист может полностью игнорировать STL и использовать стандартную библиотеку C, игнорировать ссылки, исключения и наследование. Так мы приближаемся к C с классами, за исключением того, что это не C, и вы всё ещё не понимаете указатели, выделение памяти, стек, кучу, виртуальную память. И теперь всякий раз, когда мне действительно нужен C, я должен переключиться на другой язык, который мог выучить с самого начала. Если вы собираетесь использовать C++, используйте C++. Но если вы хотите использовать C++ без всех особенностей C++, то просто изучите C. И повторю из первого абзаца, я не против C++. Мы видим бородавки на теле любимых и всё равно любим их.

И это всё, что я могу сказать об этом.

Автор: m1rko

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js