Недавно мне напомнили, почему я считаю плохой идеей давать новичкам 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. Ни спецификаторов доступа, ни базовых классов, ни пользовательских конструкторов, ни виртуальных функций. Агрегированный тип получает агрегированную инициализацию. Что это значит?
- Каждый объект класса инициализируется каждым элементом связного списка по порядку.
- Каждый объект без соответствующего связного списка элементов получит значение «инициализировано».
Отлично, что это значит? Если у объекта другой тип класса с пользовательским конструктором, будет вызван этот конструктор. Если объект является типом класса без пользовательского конструктора, как 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):
- Список инициализации для
A
приводит ко второму пункту. - Срабатывает агрегатная инициализация, поскольку A является агрегатным типом.
- Поскольку список пуст, все члены инициализируются пустыми списками.
- int i{} приводит к инициализации значения
i
, равного 0.
- int i{} приводит к инициализации значения
А если список не пуст?
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 происходит следующее:
- Список инициализации для
A
приводит ко второму пункту. - Не-агрегат с пустым braced-init-list вызывает инициализацию значения, идём к третьему пункту.
- Найден пользовательский конструктор, так что вызывается конструктор по умолчанию, который ничего не делает в этом случае,
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, даже если не вызывает инициализацию агрегата:
- Инициализация списка для A, переходим к пункту 2.
- Неагрегат, тип класса с конструктором по умолчанию, и пустой список braced-init-list вызывают инициализацию значения, переходим к пункту 3.
- Предоставленный пользователем конструктор не найден, поэтому инициализируем объект как ноль, переходим к пункту 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
не является агрегатом, поэтому происходит следующее:
- Инициализация списка для A, переходим к пункту 2.
- Поиск подходящего конструктора.
- Нет способа преобразовать
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
$
Нет! Разберёмся по пунктам:
- Инициализация списка для A, переходим к пункту 2.
- Неагрегат, тип класса с конструктором по умолчанию, и пустой список braced-init-list вызывают инициализацию значения, переходим к пункту 3.
- Не найден пользовательский конструктор по умолчанию (вот что я упустил выше), поэтому объект инициализируется как ноль, переходим к пункту 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 типов инициализации.
Кстати, я совершенно забыл, что рассуждал точно на эту тему месяц назад. Вот что делает подсознание.
Обсуждение этой статьи и критика на разных форумах:
Отвечая на самую распространённую критику: да, можно научиться разумным способам инициализации переменных и никогда не встретиться с бездной. На этот счёт я специально написал в эпилоге, что список инициализации — хорошая вещь. Лично я редко пользуюсь шаблонами, но всё равно использую C++. Дело не в этом. Дело в том, что начинающий программист может полностью игнорировать STL и использовать стандартную библиотеку C, игнорировать ссылки, исключения и наследование. Так мы приближаемся к C с классами, за исключением того, что это не C, и вы всё ещё не понимаете указатели, выделение памяти, стек, кучу, виртуальную память. И теперь всякий раз, когда мне действительно нужен C, я должен переключиться на другой язык, который мог выучить с самого начала. Если вы собираетесь использовать C++, используйте C++. Но если вы хотите использовать C++ без всех особенностей C++, то просто изучите C. И повторю из первого абзаца, я не против C++. Мы видим бородавки на теле любимых и всё равно любим их.
И это всё, что я могу сказать об этом.
Автор: m1rko