В этой статье я бы хотел рассказать о том, как работают списки инициализации (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


  1. Эта статья ознакомительная, не претендует на полноту и часто будет жертвовать корректностью в угоду понятности. С другой стороны, у читателя предполагается базовое знание C++.
  2. Я пытался придумывать разумные переводы на русский для англоязычных терминов, но с некоторыми я потерпел полное фиаско. Синтаксические конструкции вида {...} я буду называть 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 происходит в два этапа:


  1. Сначала рассматриваются только конструкторы с единственным параметром типа std::initializer_list (это один из главных моментов, когда компилятор таки генерирует std::initializer_list по содержимому фигурных скобочек). Разрешение перегрузок происходит между ними.
  2. Если ни один конструктор не подходит, то дальше все как обычно — разворачиваем 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};? Можно придумать несколько разумных стратегий:


  1. Запретить такую инициализацию вообще (в самом деле, что программист хочет этим сказать?)
  2. Вывести тип первой переменной как int, а второй вариант запретить
  3. Сделать так, чтобы и 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, который сначала добавили в стандарт, потом, не подумав, удалили, как избыточный, а потом добавили обратно в измененном виде — как описание крайнего случая с пятью (!) условиями.


Тем не менее, возможность полезная и облегчающая жизнь, поэтому здесь я приведу небольшой и не претендующий на объективность список того, как не выстрелить себе в ногу:


  1. Потратьте некоторое время на то, чтобы ознакомиться с тем, что на самом деле происходит (я рекомендую прочитать параграф стандарта — на удивление понятный и не слишком зависимый от остальных)
  2. Не используйте std::initializer_list, кроме как в параметре конструктора
  3. Да и в параметре конструктора используйте, только если вы понимаете, что происходит (если не уверены — сконструируйтесь лучше от вектора, пары итераторов или range-а)
  4. Не используйте классы-агрегаты без крайней необходимости, напишите лучше конструктор, инициализирующий все поля
  5. Не используйте braced-init-list в сочетании с auto
  6. Прочитайте эту статью про то, что делать с пустыми списками инициализации (у меня руки чешутся ее перевести и запостить, может быть, вскоре займусь)
  7. И, как я уже писал в самом начале, имейте в виду, что 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)


  1. sborisov
    07.06.2017 15:00

    Как же так, вы приводите ссылки, а сами, видимо, даже не удосужились их прочитать.
    Да ещё и всё напутали… В С++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}) оставляет все по-старому.


    1. dkozh
      07.06.2017 15:23

      Да, я опечатался и перепутал в последнем предложении copy и direct, сейчас исправлю.


    1. dkozh
      07.06.2017 16:34

      Кстати, обратите внимание, что N3922 — это defect report, и применяется не только в C++17, но и к предыдущим стандартам задним числом, что приводит к интересным результатам при апгрейде компилятора...


  1. 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, а такие вот монстры, которые пытаются решить все потенциально возможные задачи.


    1. JegernOUTT
      07.06.2017 16:28
      +1

      https://godbolt.org/g/MItwzR


      gcc 4.7.1 вышла в 12 году) clang 3.1 примерно в это же время. Так что пора уже юзать современные компиляторы)
      (про msvc не понятно, но последняя версия точно собирает)


      1. thesanone
        07.06.2017 17:10

        Иногда проекты не позволяют(


      1. Steed
        07.06.2017 17:15

        C MSVC понятно, что в 2015-й студии и ранее не работает, т.е. работает только в 2017-й. Коммерческая разработка не может позволить себе обновлять компиляторы каждые пару лет, увы: есть нестабильность только что вышедших версий, немалые трудозатраты на апгрейд и отлов вызванных им багов, стоимость лицензий (если речь о платных средах).


    1. Antervis
      07.06.2017 17:49
      +2

      То, о чем вы говорите, появилось в с++11 и исчезло в с++14. что до

      struct A {
          int x = 0;
      };
      

      То это расширение gcc, появившееся еще до с++11


    1. vagran
      07.06.2017 20:55

      Если бы не разрешили как аргументы конструктора, то нельзя было инициализировать в декларации класса члены вызовом конструктора, а это очень нужная вещь, про которую как-то совсем забыто в статье.

      class C {
      std::atomic_int refCount{1};
      };
      


  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 жадный по части перегрузок.


    1. 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, а язык поощрает такое поведение


      1. Antervis
        08.06.2017 06:17
        +1

        Скажем так: предлагаемые вами альтернативы хуже и с ними будут другие проблемы

        Практически никогда не требуется создавать локальную переменную типа std::initializer_list, а язык поощрает такое поведение

        Иногда это именно то, что требуется:
        auto values = {MyEnum1, myEnum15, MyEnum23};
        for (auto val : values) { //...
        


        Теперь мне любопытно что вас не устроило в range-based for


  1. DistortNeo
    07.06.2017 18:14
    +3

    Интересно, а почему нельзя было сделать всё однозначно, чтобы при передаче параметров в фигурных скобках вызывались только конструкторы, принимающие исключительно std::initializer_list? А все остальные конструкторы вызывались с круглыми скобками. Большая часть спорных случаев бы сразу пропала.


    1. 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 получились не очень.


      Хотя и Вашим бы предложением ничего страшного не случилось бы, как мне кажется.


    1. vagran
      07.06.2017 21:01

      Посмотрите мой комент выше по поводу инициализации членов класса конструктором. Фигурные скобки призваны в том числе устранить неднозначность синтаксиса (ambiguity), в декларации класса инициализация с круглыми скобками парсится как декларация метода. Почему-то никто не знает об этом важном моменте.


      1. DistortNeo
        07.06.2017 21:43
        +1

        Ага, посмотрел. С одной стороны, устраняется неоднозначность в одном месте. С другой стороны, добавляется в другом — ничего хорошего в этом нет. Не любой конструктор можно вызвать, используя фигурные скобки.


        После такого начинаются нравиться языки, где объявлению переменных и/или функций предшествуют ключевые слова типа var и function.


        1. vagran
          07.06.2017 21:59
          -1

          Вряд ли кто-то всерьёз утверждает, что C++ — идеальный язык. Но лично меня он вполне устраивает, если писать вдумчиво и аккуратно (auto x = {42} — ну никто ведь не будет такое в здравом уме писать), не задаваясь при этом целью подсчитать кто что где и сколько мог себе отстрелить при его использовании. Нововведение с фигурными скобками лично для меня безусловно к лучшему, при всех его минусах, в основном, кстати, из-за данной возможности вызывать конструкторы членов в декларации класса, часто этим пользуюсь.


          1. DistortNeo
            07.06.2017 22:15

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

            Ну да, просто синтаксический сахар, но выглядит все равно как костыль, а не нормальное решение.


    1. Antervis
      08.06.2017 06:25

      нельзя. int x(); — объявление функции, а не создание x default-конструктором.
      Если я хочу вызвать функцию со значением, созданным по умолчанию, я не могу использовать синтаксис Foo(());
      return (); — это что за покемон?
      Плюс к тому, большая часть спорных случаев бы пропала вместе с возможностью не писать имя класса полностью — то, для чего весь этот сыр-бор и затевался. Что лучше: сделать 30% задачи идеально или 100%, но удовлетворительно?


  1. 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")}});
    

    Это всё объяснимо и после поллитра, а то и больше, даже понятно. Но лучше бы не усложняли инициализацию. Задача сделать универсальную инициализацию на все случаи жизни так и не решена. Куча мест, где в шаблонах нельзя бездумно написать {}, иногда ещё и две версии приходится делать.


  1. 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, поэтому мы получим совсем не то, что хотели. Решение? Давайте подумаем.

    Написать так?
    auto foo = Foo()
    Нельзя, потому что конструктор копирования explicit.

    Написать так?
    auto foo(Foo{})
    Нельзя, потому что мы хотим вызвать именно конструктор по умолчанию.

    Очевидно! Написать так:
    auto foo{Foo()}


    А теперь представьте, что бы вышло без того патча: Вы бы вообще не смогли скопировать Foo в такую переменную никаким образом — пытались-пытались бы, и внезапно получили бы std::initializer_list. А точнее, Вы получили бы еще одну ошибку компиляции, потому что неявное копирование Foo запрещено.

    PS:
    Пример, конечно, совершенно бесполезный, но подобная ситуация не является чем-то невероятным.


    1. DistortNeo
      07.06.2017 22:16

      Интересно, а в каких случаях конструктор копирования приходится объявлять как explicit?


      1. alphashooter
        07.06.2017 22:46

        Например, когда у него есть серьезные side-эффекты. Ну или просто хочется запретить бездумное копирование.


        1. DistortNeo
          07.06.2017 22:54
          +1

          Тогда разумнее просто сделать Foo(const Foo&) = delete. А обдуманное копирование всегда можно реализовать какой-нибудь функцией.


          В этом случае copy-initialization ломаться не будет, а благодаря copy elision вариант auto foo = Foo() будет эквивалентен вызову конструктора по умолчанию.


          1. alphashooter
            07.06.2017 23:12

            И то правда, безусловно. Но иногда в мире случаются удивительные вещи.


          1. alphashooter
            07.06.2017 23:37

            Кстати, возможно, я Вас неправильно понял, но copy initialization не будет работать, если конструктор deleted. И неважно, возможно там copy elision или нет.


            1. DistortNeo
              07.06.2017 23:47
              +1

              Но если есть move-конструктор (в т.ч. move-конструктор по умолчанию), то будет работать.


              1. alphashooter
                08.06.2017 00:20

                А, теперь все встало на свои места.


            1. dkozh
              07.06.2017 23:52

              В C++14 — да, нужен move-constructor, причем его нужно явно написать (например, = default). В С++17, к счастью, это требование убрали, и все будет работать.


              1. alphashooter
                08.06.2017 03:40

                Ммм, не уверен. Даже открыл драфт N4296, но там требования на implicit move constructor не изменились — он все так же не объявляется, если есть user-declared copy constructor (explicit Foo(const Foo&) в данном случае).


                1. DistortNeo
                  08.06.2017 05:04
                  +1

                  Разница между C++14 и C++17 заключается именно в copy elision. Стандарт C++17 обязывает игнорировать конструкторы при copy-initialization, поэтому код будет работать всегда, если просто написать auto foo = Foo();.


                  В C++14 же для copy-initialization требуется подходящий конструктор, даже если компилятор его и выкинет.


                  1. alphashooter
                    08.06.2017 11:59

                    Да, будет работать, теперь вижу. К сожалению, из всех нововведений я в курсе лишь о тех, что случайно бросились в глаза на том же cppreference, например, потому спасибо за разъяснения.


    1. 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.


      1. 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.


        1. alphashooter
          09.06.2017 15:43

          К слову, там же объясняется причина появления defect report, который изменил поведение auto в случае direct-initialization (что мы тут и обсуждаем, собственно).


          1. dkozh
            09.06.2017 17:51
            +1

            Как раз direct-initialization из N3912 я понимаю и поддерживаю.


            А про range-based for — мне кажется, что добавить особый случай для braced-init-list именно в него было бы лучше, чем то, что получилось. Ну да это дело вкуса, видимо.