В этой статье я бы хотел рассказать о том, как работают списки инициализации (braced initializer lists) в C++, какие проблемы они были призваны решать, какие проблемы, в свою очередь, вызвали и как не попасть в просак.
Первым делом предлагаю почувствовать себя компилятором (или language lawyer-ом) и понять, компилируются ли следующие примеры, почему, и что они делают:
Классика:
std::vector<int> v1{5};
std::vector<int> v2(5);
std::vector<int> v3({5});
std::vector<int> v4{5};
std::vector<int> v5 = 5;
Современный C++ — безопасный язык, я никогда не выстрелю себе в ногу:
std::vector<std::string> x( {"a", "b"} );
std::vector<std::string> y{ {"a", "b"} };
Больше скобочек богу скобочек!
// Почему их тут пять, скомпилируется ли программа и почему?
std::vector<std::vector<int>> v1{{{{{}}}}};
Если один конструктор не подходит, мы берем второй, правильно?
struct T{};
struct S {
S(std::initializer_list<int>);
S(double, double);
S(T, T);
};
int main() {
S{T{}, T{}}; // Работает ли вот так?
S{1., 2.}; // а так?
}
Almost Always Auto, говорили они. Это повышает читабельность, говорили они:
auto x = {0}; // какой тут тип у x?
auto y{0}; // а у y?
// вы уверены? попробуйте другую версию вашего компилятора
Привет из древних времен:
struct S {
std::vector<int> a, b;
};
struct T {
std::array<int, 2> a, b;
};
int main() {
T t1{{1, 2}, {3, 4}};
T t2{1, 2, 3, 4};
T t3{1, 2};
S s1{{1, 2}, {3, 4}};
S s2{1, 2, 3, 4};
S s3{1, 2};
}
Все понятно? Или ничего не ясно? Добро пожаловать под кат.
Disclaimers
- Эта статья ознакомительная, не претендует на полноту и часто будет жертвовать корректностью в угоду понятности. С другой стороны, у читателя предполагается базовое знание C++.
- Я пытался придумывать разумные переводы на русский для англоязычных терминов, но с некоторыми я потерпел полное фиаско. Синтаксические конструкции вида
{...}
я буду называть braced-init-lists, тип из стандартной библиотеки —std::initializer_list
, а вид инициализации, когда мы пишем как-то так:int x{5}
— это list-init, также известная как uniform initialization syntax, или универсальный синтаксис инициализации.
Attention!
Первым делом обращу внимание на важное наблюдение. Даже если вы из всей статьи вынесете только его, а дальше читать станет лень, моя миссия здесь будет исполнена.
Итак, braced-init-lists (штуки с фигурными скобками, {1, 2, 3}, uniform initialization syntax) и std::initializer_list
— разные вещи! Они сильно связаны, между ними происходят всякие тонкие взаимодействия, но любое из них вполне может существовать без другого.
Но сначала — немного предыстории.
Unicorn initialization syntax
В C++98 (и его bugfix-update, C++03) существовало достаточно проблем и непоследовательностей, связанных с инициализацией. Вот некоторые из них:
- Из C пришел синтаксис инициализации переменных (в том числе, массивов и структур) с использованием фигурных скобок, но он не очень хорошо взаимодействовал с возможностями C++ (например, инициализация структур не была доступна для C++-классов)
- Часто хочется соорудить какой-нибудь контейнер (например,
std::vector
) из заранее известных элементов — в языке не было встроенной возможности для этого, а библиотечные решения (Boost.Assign
) не отличались изящностью синтаксиса, были не бесплатны с точки зрения скорости работы и не слишком хорошо влияли на время компиляции - При инициализации примитивных типов легко случайно потерять информацию при сужающем преобразовании (narrowing conversion) — например, случайно присвоить
double
вint
- Most vexing parse, которым любят пугать начинающих C++-ников.
Поэтому во время разработки C++11 родилась такая идея: давайте мы дадим возможность проинициализировать что угодно с помощью фигурных скобок:
- Для случаев, где это применимо в C, новый синтаксис будет работать так же, только лучше
- Сужающие преобразования при этом мы запретим
- А если мы пытаемся проинициализировать класс с конструкторами, то мы и конструктор сможем вызывать, с переданными параметрами
Pitfalls
Казалось бы, на этом можно и закончить: инициализация контейнеров должна получиться сама собой, ведь в C++11 появились еще и шаблоны с переменным числом параметров, так что если мы напишем variadic-конструктор… на самом деле, нет, так не получится:
- Такой конструктор должен быть шаблоном, что часто нежелательно
- Придется инстанцировать конструкторы со всевозможным числом параметров, что приведет к раздуванию кода и замедлению компиляции
- Эффективность инициализации, например, для
std::vector
-а будет все равно не идеальная
Для решения этих проблем придумали std::initializer_list
— "магический класс", который представляет собой очень легкую обертку для массива элементов известного размера, а так же умеет конструироваться от braced-init-list-а.
Почему же он "магический"? Как раз по описанным выше причинам его невозможно эффективно сконструировать в пользовательском коде, поэтому компилятор создает его специальным образом.
Зачем же он нужен? Главным образом, чтобы пользовательские классы могли сказать: "я хочу конструироваться от braced-init-list-а элементов такого-то типа", и им не требовался бы для этого шаблонный конструктор.
(Кстати, к этому моменту должно стать понятно, что std::initializer_list
и braced-init-list это разные понятия)
Теперь-то все хорошо? Мы просто добавим в наш контейнер конструктор вида vector(std::initializer_list<T>)
и все заработает? Почти.
Рассмотрим такую запись:
std::vector<int> v{5};
Что имелось в виду, v(5)
или v({5})
? Другими словами, хотим ли мы сконструировать вектор из 5 элементов, или из одного элемента со значением 5
?
Для решения этого конфликта разрешение перегрузок (overload resolution, выбор нужной функции по переданным аргументам) в случае list-initialization происходит в два этапа:
- Сначала рассматриваются только конструкторы с единственным параметром типа
std::initializer_list
(это один из главных моментов, когда компилятор таки генерируетstd::initializer_list
по содержимому фигурных скобочек). Разрешение перегрузок происходит между ними. - Если ни один конструктор не подходит, то дальше все как обычно — разворачиваем braced-init-list в список аргументов и проводим разрешение перегрузок среди всех доступных конструкторов.
Отметим, что конструктор, который проиграл на первом этапе, вполне может подойти на втором. Это объясняет пример с избытком скобочек для инициализации вектора из начала статьи. Для понятности удалим один из вложенных шаблонов, а также заменим std::vector
на свой класс:
template<typename T> struct vec {
vec(std::initializer_list<T>);
};
int main() {
vec<int> v1{{{}}};
}
Под пункт 1 наш конструктор не подходит — {{{}}}
не похож на std::initializer_list<int>
, потому что int
нельзя проинициализировать с помощью {{}}
. Однако {}
— вполне себе zero-initialization, поэтому конструктор принимается на втором шаге.
Забавно, однако, что сужающее преобразование не является достаточным поводом для того, чтобы выкинуть конструктор — в следующем примере первый конструктор принимается на первом шаге разрешения перегрузок, и потом вызывает ошибку компилятора. Хорошо это или плохо — я не знаю, для меня это просто удивительно.
struct S {
S(std::initializer_list<int>);
S(double, double);
};
int main() {
S{1., 2.};
}
Похожая проблема с довольно страшным результатом получается и в примере с вектором строк из начала статьи. К несчастью, у std::string
есть конструктор, который трактует два переданных указателя как начало и конец строки. Последствия такого поведения для строковых литералов, очевидно, плачевны, при этом синтаксически запись выглядит довольно похоже на корректный вариант и вполне может появиться, например, в обобщенном коде.
Классы-агрегаты
Ну теперь-то все? Не совсем. Старый синтаксис инициализации структур, доставшийся нам от C, никуда не делся, и можно делать так:
struct A { int i, j; };
struct B { A a1, a2; };
int main() {
B b1 = {{1, 2}, {3, 4}};
B b2 = {1, 2, 3, 4}; // brace elision
B b3 = {{1, 2}}; // clause omission
}
Как видим, при иницализации агрегатов (грубо говоря, C-подобных структур, не путать с POD, POD — это про другое) можно и пропускать вложенные скобочки, и выкидывать часть инициализаторов. Все это поведение было аккуратно перенесено в C++.
Казалось бы, какой бред, зачем это в современном языке? Давайте хотя бы предупреждения компилятора будем на это выводить, подумали разработчики GCC и clang, и были бы правы, не будь std::array
классом-агрегатом, содержащим внутри себя массив. Таким образом, предупреждение про выкидывание вложенных скобок по понятным причинам срабатывает на вот таком невинном коде:
int main() {
std::array<int, 3> a = {1,2,3};
}
Проблему эту GCC "решил" выключением соответствующего предупреждения в режиме -Wall
, в clang-е же уже три года все по-прежнему.
Кстати, тот факт, что std::array
— агрегат, не прихоть безумных авторов стандарта или ленивых разработчиков стандартных библиотек: достичь требуемой семантики этого класса просто невозможно средствами языка, не теряя в эффективности. Еще один привет от C и его странных массивов.
Возможно, большая проблема с классами-агрегатами — это не самое удачное взаимодействие с обобщенными функциями (в том числе) из стандартной библиотеки. На данный момент функции, которые конструируют объект из переданных параметров (например, vector::emplace_back
или make_unique
), вызывают обычную инициализацию, не "универсальную". Вызвано это тем, что использование list-initialization не позволяет никаким нормальным способом вызвать "обычный" контруктор вместо принимающего std::initializer_list
(примерно та же проблема, что и с инициализацией в не-шаблонном коде, только тут пользователь не может обойти ее вызовом другого конструктора). Работа в этом направлении ведется, но пока мы имеем то, что имеем.
Almost Always Auto
Как же braced-init-list-ы ведут себя в сочетании с выводом типов? Что будет, если я напишу auto x = {0}; auto y = {1, 2};
? Можно придумать несколько разумных стратегий:
- Запретить такую инициализацию вообще (в самом деле, что программист хочет этим сказать?)
- Вывести тип первой переменной как
int
, а второй вариант запретить - Сделать так, чтобы и x, и y имели тип
std::initializer_lits<int>
Последний вариант нравится мне меньше всего (мало кому в реальной жизни заводить локальные переменные типа std::initializer_list
), но в стандарт С++11 попал именно он. Постепенно стало выясняться, что это вызывает проблемы у программистов (кто бы мог подумать), поэтому в стандарт добавили патч http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3922.html, который реализует поведение №2… только в случае direct-list-initialization (auto x{5}
), а в случае copy-list-initialization (auto x = {5}
) оставляет все по-старому.
Я не могу это комментирвать. По-моему, это один из очень редких случаев, когда здравый смысл временно покинул авторов языка. Если у вас есть, что сказать по этому поводу, сообщите мне об этом в комментариях.
Промежуточные итоги
Хотя универсальный синтаксис инициализации и std::initializer_list
— возможности языка, добавленные из благих и правильных побуждений, мне кажется, что из-за извечной необходимости в обратной совместимости и не всегда дальновидных решениях на ранних этапах вся ситуация вокруг них на данный момент излишне сложная, вымученная и не самая приятная для всех вовлеченных сторон — авторов стандарта, компиляторов, библиотек и прикладных разработчиков. Хотели как лучше, а получилось, как в известном комиксе:
В качестве примера возьмем, например, историю с [over.best.ics]/4.5, который сначала добавили в стандарт, потом, не подумав, удалили, как избыточный, а потом добавили обратно в измененном виде — как описание крайнего случая с пятью (!) условиями.
Тем не менее, возможность полезная и облегчающая жизнь, поэтому здесь я приведу небольшой и не претендующий на объективность список того, как не выстрелить себе в ногу:
- Потратьте некоторое время на то, чтобы ознакомиться с тем, что на самом деле происходит (я рекомендую прочитать параграф стандарта — на удивление понятный и не слишком зависимый от остальных)
- Не используйте
std::initializer_list
, кроме как в параметре конструктора - Да и в параметре конструктора используйте, только если вы понимаете, что происходит (если не уверены — сконструируйтесь лучше от вектора, пары итераторов или range-а)
- Не используйте классы-агрегаты без крайней необходимости, напишите лучше конструктор, инициализирующий все поля
- Не используйте braced-init-list в сочетании с
auto
- Прочитайте эту статью про то, что делать с пустыми списками инициализации (у меня руки чешутся ее перевести и запостить, может быть, вскоре займусь)
- И, как я уже писал в самом начале, имейте в виду, что braced-init-list и
std::initializer_list
— это разные концепции, весьма хитро взаимодействующие друг с другом
Давайте помечтаем
Тут я заканчиваю свое введение в существующее положение дел, и хочу вбросить помечтать, как все могло бы быть, если бы мы жили в идеальном мире.
Мне кажется, что переиспользовать фигурные скобки для создания std::initializer_list
во время инициализации — ошибка дизайна языка. Я был бы очень рад, если бы вместо этого мы получили бы более явный и отдельный синтаксис (пусть и более уродливый, например, какие-нибудь странные скобки типа <$...$>
или встроенный интринзик вроде std::of(...)
). То есть инициализируем вектор как-то так: std::vector<std::vector<int>> x = std::of(std::of(1, 2), std::of(3, 4));
Что бы это дало? Новый способ инициализации (с защитой от most vexing parse и сужающих преобразований) оказался бы отвязан от std::initializer_list
, не потребовалось бы вводить отдельный шаг для разрешения перегрузок, ушла бы проблема с конструктором vector<int>
или vector<string>
, новый синтаксис инициализации можно было бы использовать в обобщенном коде безо всяких проблем.
Конечно, недостатки у такого подхода довольно серьезные: более уродливый синтаксис в простейших случаях и уход от цели сделать синтаксис более унифицированным с инициализацией в стиле C (к такой унификации я отношусь довольно скептически, но это тема для отдельного разговора).
Еще я недолюбливаю классы-агрегаты. Если оставить за скобками проблему с std::array
, я не вижу достойного обоснования для существования такой большой и особенной возможности языка. Проблему с тем, что программистам не хочется писать тривиальные конструкторы для простых классов, можно было бы решить менее инвазивными способами, например, дать возможность генерировать конструктор, который бы инициализировал все поля по очереди:
struct S {
int a, b;
S(...) = aggregate;
};
Заключение
Напоследок повторюсь еще раз, что я не претендую на 100% корректность или на истину в последней инстанции. Добро пожаловать в комментарии, если что-то осталось непонятным, или если есть что сказать по этой довольно специфической теме.
Комментарии (36)
Steed
07.06.2017 15:24С классами-агрегатами вообще обидно вышло. Думаешь: "Ура, наконец-то я могу инициализировать 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
, а такие вот монстры, которые пытаются решить все потенциально возможные задачи.JegernOUTT
07.06.2017 16:28+1
gcc 4.7.1 вышла в 12 году) clang 3.1 примерно в это же время. Так что пора уже юзать современные компиляторы)
(про msvc не понятно, но последняя версия точно собирает)Steed
07.06.2017 17:15C MSVC понятно, что в 2015-й студии и ранее не работает, т.е. работает только в 2017-й. Коммерческая разработка не может позволить себе обновлять компиляторы каждые пару лет, увы: есть нестабильность только что вышедших версий, немалые трудозатраты на апгрейд и отлов вызванных им багов, стоимость лицензий (если речь о платных средах).
Antervis
07.06.2017 17:49+2То, о чем вы говорите, появилось в с++11 и исчезло в с++14. что до
struct A { int x = 0; };
То это расширение gcc, появившееся еще до с++11
vagran
07.06.2017 20:55Если бы не разрешили как аргументы конструктора, то нельзя было инициализировать в декларации класса члены вызовом конструктора, а это очень нужная вещь, про которую как-то совсем забыто в статье.
class C { std::atomic_int refCount{1}; };
Antervis
07.06.2017 18:08+2который реализует поведение №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 жадный по части перегрузок.dkozh
07.06.2017 18:36+2Тогда я, пожалуй, прокомментирую, почему мне это не нравится :)
- Это не очень консистентно:
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
, а язык поощрает такое поведение
Antervis
08.06.2017 06:17+1Скажем так: предлагаемые вами альтернативы хуже и с ними будут другие проблемы
Практически никогда не требуется создавать локальную переменную типа std::initializer_list, а язык поощрает такое поведение
Иногда это именно то, что требуется:
auto values = {MyEnum1, myEnum15, MyEnum23}; for (auto val : values) { //...
Теперь мне любопытно что вас не устроило в range-based for
- Это не очень консистентно:
DistortNeo
07.06.2017 18:14+3Интересно, а почему нельзя было сделать всё однозначно, чтобы при передаче параметров в фигурных скобках вызывались только конструкторы, принимающие исключительно
std::initializer_list
? А все остальные конструкторы вызывались с круглыми скобками. Большая часть спорных случаев бы сразу пропала.dkozh
07.06.2017 18:47Можно пойти еще дальше и вообще не вводить list-initialization в таком виде, а конструкторы вызывать всегда скобочками, например
std::vector<int> v({1, 2, 3});
.
Но тогда бы не были достигнуты другие цели:
- Универсальность (иначе инициализация C-структур, масисвов и примитивных типов через фигурные скобки осталась бы странным частным случаем)
- Защита от most vexing parse
- Защита от narrowing conversions
Стоили ли эти цели того, что получилось? Насколько я понимаю, в сообществе нет консенсуса по этому поводу. Мое мнение я написал в статье — сама по себе list-initialization — ок, а вот правила про
std::initializer_list
получились не очень.
Хотя и Вашим бы предложением ничего страшного не случилось бы, как мне кажется.
vagran
07.06.2017 21:01Посмотрите мой комент выше по поводу инициализации членов класса конструктором. Фигурные скобки призваны в том числе устранить неднозначность синтаксиса (ambiguity), в декларации класса инициализация с круглыми скобками парсится как декларация метода. Почему-то никто не знает об этом важном моменте.
DistortNeo
07.06.2017 21:43+1Ага, посмотрел. С одной стороны, устраняется неоднозначность в одном месте. С другой стороны, добавляется в другом — ничего хорошего в этом нет. Не любой конструктор можно вызвать, используя фигурные скобки.
После такого начинаются нравиться языки, где объявлению переменных и/или функций предшествуют ключевые слова типа var и function.
vagran
07.06.2017 21:59-1Вряд ли кто-то всерьёз утверждает, что C++ — идеальный язык. Но лично меня он вполне устраивает, если писать вдумчиво и аккуратно (auto x = {42} — ну никто ведь не будет такое в здравом уме писать), не задаваясь при этом целью подсчитать кто что где и сколько мог себе отстрелить при его использовании. Нововведение с фигурными скобками лично для меня безусловно к лучшему, при всех его минусах, в основном, кстати, из-за данной возможности вызывать конструкторы членов в декларации класса, часто этим пользуюсь.
DistortNeo
07.06.2017 22:15Нововведение с фигурными скобками лично для меня безусловно к лучшему, при всех его минусах, в основном, кстати, из-за данной возможности вызывать конструкторы членов в декларации класса, часто этим пользуюсь.
Ну да, просто синтаксический сахар, но выглядит все равно как костыль, а не нормальное решение.
Antervis
08.06.2017 06:25нельзя. int x(); — объявление функции, а не создание x default-конструктором.
Если я хочу вызвать функцию со значением, созданным по умолчанию, я не могу использовать синтаксис Foo(());
return (); — это что за покемон?
Плюс к тому, большая часть спорных случаев бы пропала вместе с возможностью не писать имя класса полностью — то, для чего весь этот сыр-бор и затевался. Что лучше: сделать 30% задачи идеально или 100%, но удовлетворительно?
bfDeveloper
07.06.2017 19:07+4Правила перегрузки со списками инициализации вообще безумные. То, что один элемент списка приводит к поиску других конструкторов, регулярно ломает код. Например:
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")}});
Это всё объяснимо и после поллитра, а то и больше, даже понятно. Но лучше бы не усложняли инициализацию. Задача сделать универсальную инициализацию на все случаи жизни так и не решена. Куча мест, где в шаблонах нельзя бездумно написать {}, иногда ещё и две версии приходится делать.
alphashooter
07.06.2017 20:02-1По-моему, это один из очень редких случаев, когда здравый смысл временно покинул авторов языка.
Честно, я не припомню ни одного случая, когда здравый смысл бы их покинул.
Попробуйте скомпилировать вот этот абстрактный пример (прошу обратить внимание на конструкторы):
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, поэтому мы получим совсем не то, что хотели. Решение? Давайте подумаем.
Написать так?
Нельзя, потому что конструктор копирования explicit.auto foo = Foo()
Написать так?
Нельзя, потому что мы хотим вызвать именно конструктор по умолчанию.auto foo(Foo{})
Очевидно! Написать так:
auto foo{Foo()}
А теперь представьте, что бы вышло без того патча: Вы бы вообще не смогли скопировать Foo в такую переменную никаким образом — пытались-пытались бы, и внезапно получили бы std::initializer_list. А точнее, Вы получили бы еще одну ошибку компиляции, потому что неявное копирование Foo запрещено.
PS:
Пример, конечно, совершенно бесполезный, но подобная ситуация не является чем-то невероятным.DistortNeo
07.06.2017 22:16Интересно, а в каких случаях конструктор копирования приходится объявлять как
explicit
?alphashooter
07.06.2017 22:46Например, когда у него есть серьезные side-эффекты. Ну или просто хочется запретить бездумное копирование.
DistortNeo
07.06.2017 22:54+1Тогда разумнее просто сделать
Foo(const Foo&) = delete
. А обдуманное копирование всегда можно реализовать какой-нибудь функцией.
В этом случае copy-initialization ломаться не будет, а благодаря copy elision вариант
auto foo = Foo()
будет эквивалентен вызову конструктора по умолчанию.alphashooter
07.06.2017 23:37Кстати, возможно, я Вас неправильно понял, но copy initialization не будет работать, если конструктор deleted. И неважно, возможно там copy elision или нет.
DistortNeo
07.06.2017 23:47+1Но если есть move-конструктор (в т.ч. move-конструктор по умолчанию), то будет работать.
dkozh
07.06.2017 23:52В C++14 — да, нужен move-constructor, причем его нужно явно написать (например,
= default
). В С++17, к счастью, это требование убрали, и все будет работать.alphashooter
08.06.2017 03:40Ммм, не уверен. Даже открыл драфт N4296, но там требования на implicit move constructor не изменились — он все так же не объявляется, если есть user-declared copy constructor (
explicit Foo(const Foo&)
в данном случае).DistortNeo
08.06.2017 05:04+1Разница между C++14 и C++17 заключается именно в copy elision. Стандарт C++17 обязывает игнорировать конструкторы при copy-initialization, поэтому код будет работать всегда, если просто написать
auto foo = Foo();
.
В C++14 же для copy-initialization требуется подходящий конструктор, даже если компилятор его и выкинет.
alphashooter
08.06.2017 11:59Да, будет работать, теперь вижу. К сожалению, из всех нововведений я в курсе лишь о тех, что случайно бросились в глаза на том же cppreference, например, потому спасибо за разъяснения.
dkozh
07.06.2017 23:34+1Да, я за то, чтобы вообще запретить выводить
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.
alphashooter
09.06.2017 15:29В 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.
alphashooter
09.06.2017 15:43К слову, там же объясняется причина появления defect report, который изменил поведение auto в случае direct-initialization (что мы тут и обсуждаем, собственно).
dkozh
09.06.2017 17:51+1Как раз direct-initialization из N3912 я понимаю и поддерживаю.
А про range-based for — мне кажется, что добавить особый случай для braced-init-list именно в него было бы лучше, чем то, что получилось. Ну да это дело вкуса, видимо.
sborisov
Как же так, вы приводите ссылки, а сами, видимо, даже не удосужились их прочитать.
Да ещё и всё напутали… В С++17 в вашем примере:
auto y = {1, 2}; // decltype(н) is std::initializer_list
обычный int, будето только при прямой инициализации
auto x5{ 3 }; // decltype(x5) is int
dkozh
Да, я опечатался и перепутал в последнем предложении copy и direct, сейчас исправлю.
dkozh
Кстати, обратите внимание, что N3922 — это defect report, и применяется не только в C++17, но и к предыдущим стандартам задним числом, что приводит к интересным результатам при апгрейде компилятора...