Почему стандарт различает инициализацию прямого списка и инициализацию списка копий?

Мы знаем, что T v(x); называется прямой инициализацией, а T v = x; называется copy-initialization, что означает, что он построит временный T из x, который будет скопирован/перенесен в v ( который, скорее всего, уклонился).

Для инициализации списка, стандарт различает две формы, в зависимости от контекста. T v{x}; называется direct-list-initialization, а T v = {x}; называется copy-list-initialization:

§8.5.4 [dcl.init.list] p1

[...] Инициализация списка может возникать при прямой инициализации или инициализации копирования контексты; инициализация списка в контексте прямой инициализации называется инициализацией прямого списка, а инициализация списка в контексте инициализации копирования называется copy-list-initialization. [...]

Однако по всему стандарту есть только две ссылки. Для инициализации прямого списка он упоминается при создании временных файлов, таких как T{x} (§5.2.3/3). Для инициализации списка-экземпляра для выражения в выражениях return, таких как return {x}; (§6.6.3/2).

Теперь, как насчет следующего фрагмента?

#include <initializer_list>

struct X{
  X(X const&) = delete; // no copy
  X(X&&) = delete; // no move
  X(std::initializer_list<int>){} // only list-init from 'int's
};

int main(){
  X x = {42};
}

Как правило, из шаблона X x = expr; мы ожидаем, что код не скомпилируется, потому что конструктор перемещения x определяется как delete d. Тем не менее, последние версии Clang и GCC компилируют приведенный выше код очень хорошо, и после копания бит (и нахождения вышеуказанной цитаты) это кажется правильным поведением. Стандарт только когда-либо определяет поведение для всей инициализации списка и не проводит различий между этими двумя формами вообще, за исключением вышеупомянутых пунктов. Ну, по крайней мере, насколько я вижу, в любом случае.

Итак, чтобы подытожить мой вопрос еще раз:

Какая польза от разбиения списка-инициализации на две формы, если они (по-видимому) делают то же самое?

Ответы

Ответ 1

Потому что они не делают то же самое. Как указано в 13.3.1.7 [over.match.list]:

В инициализации списка копий, если выбран явный конструктор, инициализация плохо сформирована.

Короче говоря, вы можете использовать неявное преобразование в контекстах списка-инициализации списка.

Это было явно добавлено, чтобы сделать равномерную инициализацию not, um, равномерной. Да, я знаю, насколько глупо это звучит, но медведь со мной.

В 2008 году опубликован N2640 (PDF), взглянув на текущее состояние равномерной инициализации. Он смотрел конкретно на разницу между прямой инициализацией (T{...}) и копированием-инициализацией (T = {...}).

Подводя итог, было высказано опасение, что конструкторы explicit фактически станут бессмысленными. Если у меня есть какой-то тип T, который я хочу построить из целого числа, но я не хочу неявного преобразования, я ярлык явного конструктора.

Затем кто-то делает это:

T func()
{
  return {1};
}

Без текущей формулировки это вызовет мой конструктор explicit. Так что же полезно сделать конструктор explicit, если он не сильно изменится?

В текущей редакции вам нужно, по крайней мере, использовать имя напрямую:

T func()
{
  return T{1};
}