Comments 38
Да ещё и всё напутали… В С++17 в вашем примере:
- auto x = {0}; // decltype(x) is std::initializer_list
auto y = {1, 2}; // decltype(н) is std::initializer_list
обычный int, будето только при прямой инициализации
auto x5{ 3 }; // decltype(x5) is int
Что будет, если я напишу auto x = {0}; auto y = {1, 2};? Можно придумать несколько разумных стратегий:
Запретить такую инициализацию вообще (в самом деле, что программист хочет этим сказать?)
Вывести тип первой переменной как int, а второй вариант запретить
Сделать так, чтобы и x, и y имели тип std::initializer_litsПоследний вариант нравится мне меньше всего (мало кому в реальной жизни заводить локальные переменные типа std::initializer_list), но в стандарт С++11 попал именно он. Постепенно стало выясняться, что это вызывает проблемы у программистов (кто бы мог подумать), поэтому в стандарт добавили патч http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3922.html, который реализует поведение №2… только в случае copy-list-initialization (auto x = {5}), а в случае direct-list-initialization (auto x{5}) оставляет все по-старому.
Да, я опечатался и перепутал в последнем предложении copy и direct, сейчас исправлю.
Кстати, обратите внимание, что N3922 — это defect report, и применяется не только в C++17, но и к предыдущим стандартам задним числом, что приводит к интересным результатам при апгрейде компилятора...
С классами-агрегатами вообще обидно вышло. Думаешь: "Ура, наконец-то я могу инициализировать non-POD структуры фигурными скобками, даёшь!" А потом выходит, что шаг вправо. шаг влево — нельзя. Например, aggreate initializtion незьзя использвать, если есть конутруктор или значения по умолчанию для полей (т.н. default member initializers), т.е. нельзя сказать
struct A {
int x = 0;
};
A obj{1};
а если отказаться от значений по умолчанию, кто-то может создать структуру в виде A obj;
и получить неинициализированные поля.
К счастью, в 14'м стандарте одумались и member initializers разрешили (но до него надо еще дожить, точнее доапгрейдиться).
В своих лекциях про C++11 Scott Meyers говорил, что когда возникает новая хорошая идея (инициализировать массивы списком элементов через {}
), комитет сразу думает: "ага, а давайте добавим это везде!" (разрешим в фигурных скобках писать аргументы конструктора). В итоге получается не локальная фишечка типа описанного вами std::of
, а такие вот монстры, которые пытаются решить все потенциально возможные задачи.
gcc 4.7.1 вышла в 12 году) clang 3.1 примерно в это же время. Так что пора уже юзать современные компиляторы)
(про msvc не понятно, но последняя версия точно собирает)
C MSVC понятно, что в 2015-й студии и ранее не работает, т.е. работает только в 2017-й. Коммерческая разработка не может позволить себе обновлять компиляторы каждые пару лет, увы: есть нестабильность только что вышедших версий, немалые трудозатраты на апгрейд и отлов вызванных им багов, стоимость лицензий (если речь о платных средах).
struct A {
int x = 0;
};
То это расширение gcc, появившееся еще до с++11
class C {
std::atomic_int refCount{1};
};
который реализует поведение №2… только в случае direct-list-initialization (auto x{5}), а в случае copy-list-initialization (auto x = {5}) оставляет все по-старому.
Я не могу это комментирвать. По-моему, это один из очень редких случаев, когда здравый смысл временно покинул авторов языка.
сколько людей, столько и мнений. Когда я изучал с++11/с++14, мне новый вариант показался до боли логичным. Смотрите: в auto x = {1}; тип правой части ({1}) — initializer_list, а тип левой части такой же, как и тип правой. А выражение auto x {1}; — «создать переменную выведенного типа, проинициализировав её значением 1».
Если один конструктор не подходит, мы берем второй, правильно?
Просто надо знать, что конструктор от initializer_list жадный по части перегрузок.
Тогда я, пожалуй, прокомментирую, почему мне это не нравится :)
- Это не очень консистентно:
auto x = 5;
иauto x(5);
значат одно и то же,auto x = {5};
иauto x{5};
— разное, а что делать сauto x({5});
— вообще не очень ясно - Такая запись прививает ложное чувство, что
{1}
— это выражение с типомstd::initializer_list<int>
, что не так. Это вообще не выражение, у него нет типа, а такое его использование с auto-переменными — отдельно прописанное исключение (второе такое же — это range-based for). Мне кажется, исключений в C++ и так достаточно :) - Практически никогда не требуется создавать локальную переменную типа
std::initializer_list
, а язык поощрает такое поведение
Практически никогда не требуется создавать локальную переменную типа std::initializer_list, а язык поощрает такое поведение
Иногда это именно то, что требуется:
auto values = {MyEnum1, myEnum15, MyEnum23};
for (auto val : values) { //...
Теперь мне любопытно что вас не устроило в range-based for
Интересно, а почему нельзя было сделать всё однозначно, чтобы при передаче параметров в фигурных скобках вызывались только конструкторы, принимающие исключительно std::initializer_list
? А все остальные конструкторы вызывались с круглыми скобками. Большая часть спорных случаев бы сразу пропала.
Можно пойти еще дальше и вообще не вводить list-initialization в таком виде, а конструкторы вызывать всегда скобочками, например std::vector<int> v({1, 2, 3});
.
Но тогда бы не были достигнуты другие цели:
- Универсальность (иначе инициализация C-структур, масисвов и примитивных типов через фигурные скобки осталась бы странным частным случаем)
- Защита от most vexing parse
- Защита от narrowing conversions
Стоили ли эти цели того, что получилось? Насколько я понимаю, в сообществе нет консенсуса по этому поводу. Мое мнение я написал в статье — сама по себе list-initialization — ок, а вот правила про std::initializer_list
получились не очень.
Хотя и Вашим бы предложением ничего страшного не случилось бы, как мне кажется.
Ага, посмотрел. С одной стороны, устраняется неоднозначность в одном месте. С другой стороны, добавляется в другом — ничего хорошего в этом нет. Не любой конструктор можно вызвать, используя фигурные скобки.
После такого начинаются нравиться языки, где объявлению переменных и/или функций предшествуют ключевые слова типа var и function.
Нововведение с фигурными скобками лично для меня безусловно к лучшему, при всех его минусах, в основном, кстати, из-за данной возможности вызывать конструкторы членов в декларации класса, часто этим пользуюсь.
Ну да, просто синтаксический сахар, но выглядит все равно как костыль, а не нормальное решение.
Если я хочу вызвать функцию со значением, созданным по умолчанию, я не могу использовать синтаксис Foo(());
return (); — это что за покемон?
Плюс к тому, большая часть спорных случаев бы пропала вместе с возможностью не писать имя класса полностью — то, для чего весь этот сыр-бор и затевался. Что лучше: сделать 30% задачи идеально или 100%, но удовлетворительно?
struct InitMap {
using Map = map<string, string>;
InitMap(Map m) {}
};
InitMap m({{"k", "v"}});
Компилятору непонятно, то ли конструктор копирования звать, то ли конструктор от map. Добавляем «пустой» элемент:
InitMap m({{"k", "v"}, {});
И всё работает. Самая магия, что вариант
InitMap m({{string("k"), "v"}});
Работает, а приведение обоих элементов нет:
InitMap m({{string("k"), string("v")}});
Это всё объяснимо и после поллитра, а то и больше, даже понятно. Но лучше бы не усложняли инициализацию. Задача сделать универсальную инициализацию на все случаи жизни так и не решена. Куча мест, где в шаблонах нельзя бездумно написать {}, иногда ещё и две версии приходится делать.
По-моему, это один из очень редких случаев, когда здравый смысл временно покинул авторов языка.
Честно, я не припомню ни одного случая, когда здравый смысл бы их покинул.
Попробуйте скомпилировать вот этот абстрактный пример (прошу обратить внимание на конструкторы):
struct Foo {
Foo() {
// unpredictable logic
}
Foo(std::initializer_list<int>) {
// more unpredictable logic
}
explicit Foo(const Foo&) {
// even more unpredictable logic
}
};
void main() {
auto foo(Foo());
std::cout << typeid(foo).name() << std::endl;
}
Подсказка: он не скомпилируется. Почему? Да-да, тот самый most vexing parse, поэтому мы получим совсем не то, что хотели. Решение? Давайте подумаем.
Написать так?
auto foo = Foo()
Нельзя, потому что конструктор копирования explicit.Написать так?
auto foo(Foo{})
Нельзя, потому что мы хотим вызвать именно конструктор по умолчанию.Очевидно! Написать так:
auto foo{Foo()}
А теперь представьте, что бы вышло без того патча: Вы бы вообще не смогли скопировать Foo в такую переменную никаким образом — пытались-пытались бы, и внезапно получили бы std::initializer_list. А точнее, Вы получили бы еще одну ошибку компиляции, потому что неявное копирование Foo запрещено.
PS:
Пример, конечно, совершенно бесполезный, но подобная ситуация не является чем-то невероятным.
Интересно, а в каких случаях конструктор копирования приходится объявлять как explicit
?
Тогда разумнее просто сделать Foo(const Foo&) = delete
. А обдуманное копирование всегда можно реализовать какой-нибудь функцией.
В этом случае copy-initialization ломаться не будет, а благодаря copy elision вариант auto foo = Foo()
будет эквивалентен вызову конструктора по умолчанию.
Но если есть move-конструктор (в т.ч. move-конструктор по умолчанию), то будет работать.
В C++14 — да, нужен move-constructor, причем его нужно явно написать (например, = default
). В С++17, к счастью, это требование убрали, и все будет работать.
Ммм, не уверен. Даже открыл драфт N4296, но там требования на implicit move constructor не изменились — он все так же не объявляется, если есть user-declared copy constructor (explicit Foo(const Foo&)
в данном случае).
Разница между C++14 и C++17 заключается именно в copy elision. Стандарт C++17 обязывает игнорировать конструкторы при copy-initialization, поэтому код будет работать всегда, если просто написать auto foo = Foo();
.
В C++14 же для copy-initialization требуется подходящий конструктор, даже если компилятор его и выкинет.
Да, я за то, чтобы вообще запретить выводить std::initializer_list
для auto
переменных, это и пытался изложить в статье.
Наверняка текущий странный вариант не так просто приняли, но причин я пока не понял. Все, что есть в n3922 — это параграф, который не объясняет проблем:
There was also support (though less strong) in EWG for an alternative proposal in which auto deduction does not distinguish between direct list-initialization and copy list-initialization, and which allows auto deduction from a braced-init-list only in the case where the braced-init-list consists of a single item. Possible wording was for that alternative was included in N3681 by Voutilainen.
В N3922 есть следующая отсылка:
For background information see N3681, "Auto and braced-init-lists", by Voutilainen, and N3912, "Auto and braced-init-lists, continued", also by Voutilainen.
При этом в N3912, хоть и без лишних подробностей, объясняется почему и как было принято именно такое решение. Основная причина, я так понимаю, в том, чтобы не сломать range-based for в случае, когда в качестве range expression используется braced-init-list.
К слову, там же объясняется причина появления defect report, который изменил поведение auto в случае direct-initialization (что мы тут и обсуждаем, собственно).
потому что int нельзя проинициализировать с помощью {{}}
почему же нельзя? можно:
int x{{}};
Не люблю инициализаторы в стиле с++, потому, что не видно, что куда идет. Т.е., использовать можно для простых структур, а если нужно инициализировать 3 и больше полей - то уже неясно, что куда присваивается. В этом плане лучше продуман стиль С99, синтаксис похож на раст: `Foo {.a=20, .b=30, .c=40,....}`
Списки инициализации в C++: хороший, плохой, злой