Почему стандарт различает прямую инициализацию списка и инициализацию копирования списка?
Мы знаем это T v(x);
называется прямой инициализацией, в то время как T v = x;
называется copy-initialization, что означает, что он создаст временный T
от x
который будет скопирован / перемещен в v
(который, скорее всего, исключен).
Для инициализации списка стандарт различает две формы в зависимости от контекста. T v{x};
называется прямой инициализацией списка в то время как T v = {x};
называется copy-list-initialization:
§8.5.4 [dcl.init.list] p1
[...] Инициализация списка может происходить в контексте прямой инициализации или инициализации копирования; инициализация списка в контексте прямой инициализации называется прямой инициализацией списка, а инициализация списка в контексте инициализации копирования называется инициализацией копирования списка. [...]
Однако в целом стандарте есть только две ссылки. Для прямой инициализации списка это упоминается при создании временных T{x}
(§5.2.3/3
). Для копирования списка инициализации, это для выражения в операторах возврата, таких как 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
д. Тем не менее, последние версии Clang и GCC прекрасно компилируют приведенный выше код, и после того, как он немного покопался (и нашел приведенную выше цитату), это кажется правильным поведением. Стандарт только когда-либо определяет поведение для всей инициализации списка, и вообще не проводит различия между двумя формами, за исключением вышеупомянутых пунктов. Ну, по крайней мере, насколько я вижу, в любом случае.
Итак, чтобы подвести итог моего вопроса еще раз:
Какая польза от разделения инициализации списка на две его формы, если они (по-видимому) делают одно и то же?
1 ответ
Потому что они не делают то же самое. Как указано в 13.3.1.7 [over.match.list]:
При инициализации copy-list-init, если выбран явный конструктор, инициализация некорректна.
Короче говоря, вы можете использовать неявное преобразование только в контексте инициализации копирования списка.
Это было явно добавлено, чтобы сделать равномерную инициализацию не единой. Да, я знаю, как это звучит глупо, но терпите меня.
В 2008 году была опубликована N2640 (PDF), в которой рассматривается текущее состояние равномерной инициализации. Посмотрел конкретно на разницу между прямой инициализацией (T{...}
) и копия-инициализация (T = {...}
).
Подводя итог, проблема была в том, что explicit
конструкторы фактически станут бессмысленными. Если у меня есть какой-то тип T
что я хочу иметь возможность быть построенным из целого числа, но я не хочу неявного преобразования, я помечаю конструктор как явный.
Тогда кто-то делает это:
T func()
{
return {1};
}
Без текущей формулировки, это будет называть мой explicit
конструктор. Так что хорошего в том, чтобы сделать конструктор explicit
если это не сильно изменится?
С текущей формулировкой вам нужно как минимум использовать имя напрямую:
T func()
{
return T{1};
}