Почему стандарт различает инициализацию прямого списка и инициализацию списка копий?
Мы знаем, что 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};
}