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

Единая инициализация — важная и полезная функция C++11. Однако вы не можете просто использовать {} везде, так как:

std::vector<int> a(10, 0);    // 10 elements of value zero
std::vector<int> b({10, 0});  // 2 elements of value 10 and 0 respectively
std::vector<int> c{10, 0};    // 2 elements of value 10 and 0 respectively
std::vector<int> d = {10, 0}; // 2 elements of value 10 and 0 respectively

auto e(0);    // deduced type is int
auto f = 0;   // deduced type is int
auto g{0};    // deduced type is std::initializer_list<int>
auto h = {0}; // deduced type is std::initializer_list<int>

Отметив, что агрегатная инициализация, например, std::arrays требует использования {{}}, мне кажется, что всей проблемы с какой векторный конструктор будет выбран можно было бы избежать, потребовав {{}} для вызова конструкторов, принимающих std::initializer_list:

std::vector<int> i{10, 0};    // 10 elements of value zero
std::vector<int> j{{10, 0}};  // 2 elements of value 10 and 0 respectively
std::vector<int> k = {10, 0}; // 2 elements of value 10 and 0 respectively

auto l{0};    // deduced type is int
auto m{{0}};  // deduced type is std::initializer_list<int>
auto n = {0}; // deduced type is std::initializer_list<int>

Я уверен, что это обсуждалось, так каковы были причины против этого? В качестве ответа предпочтительна цитата/ссылка из стандартного предложения.

Обновление. В N2532 есть пункт, который состояния:

(3) Вероятные неприятные случаи двусмысленности возникают только для коротких списков инициализаторов [...]

(5) Почему языковые правила должны заставлять программистов, которые хотят контроля над краткостью и двусмысленностью (по вполне веским причинам), писать больше, чтобы угодить программистам, которые предпочитают (по вполне веским причинам) быть более явными — и могут быть?

[...]

Предположим, что программист ожидает вызова функции f(X). Как f(Y) может «перехватить» колл?

(4) Предположим, что у X нет конструктора списка инициализаторов, а у Y есть. В этом случае приоритет отдается конструкторам списка инициализаторов в пользу угонщика (помните, мы предполагали, что программист каким-то образом ожидал вызова f(X)). Это аналогично тому, как кто-то ожидает, что f(y) вызовет f(X) с помощью определяемого пользователем преобразования, и кто-то приходит с f(Y), который точно соответствует. Я думаю, было бы справедливо ожидать, что кто-то, кто использует {…}, вспомнит о возможности конструкторов списков инициализаторов. [выделено мной]

Я предполагаю, что ключ заключается в может быть, что означает, что вам не нужно использовать универсальную инициализацию. Правильно использовать {} сложно, так как:

  • вам нужно проверить не только конструктор, который вы хотите вызвать, но и любой конструктор, принимающий initializer_list, который может победить (и, вероятно, выиграет) над ним;

  • если вы пишете код, используя {}, а кто-то в будущем добавит конструктор std::initializer_list, ваш код может сломаться и сделать это молча.

Даже если у вас есть класс A с конструкторами A(int, bool) и A(std::initializer_list<double>), последний будет выбран вместо первого для A a{0, false}; (что, по-моему, безумие), поэтому мне действительно сложно использовать унифицированную инициализацию для классов которые имеют или могут иметь (требуются сверхспособности хрустального шара) initializer_list конструкторов.

Тот факт, что ваш код может незаметно сломаться, меня очень беспокоит.


person gnzlbg    schedule 19.03.2014    source источник
comment
Я не уверен, какую проблему вы пытаетесь решить. Вы просто показываете другой способ ведения дел. Единственная проблема, которую я вижу, это std::array, возможно, требующая двух наборов фигурных скобок. Но я думаю, что это исправляется.   -  person juanchopanza    schedule 19.03.2014
comment
Проблема, которую я пытаюсь решить, заключается в том, чтобы выяснить, почему этот другой способ ведения дел не был стандартизирован (а текущий был). В частности, мне, вероятно, понадобится причина, стоящая за этим решением, или хорошие контрпримеры против него, чтобы быть удовлетворенным.   -  person gnzlbg    schedule 19.03.2014
comment
Я не думаю, что кто-то хотел бы, чтобы auto e{0, 0}; было ошибкой. Я также не думаю, что кто-то хотел бы, чтобы это рассматривалось как применение оператора запятой. Единственный разумный тип для этого — std::initializer_list<int> (или что-то вроде int[]). Но если auto e{(list of ints)}; должно быть разрешено как std::initializer_list<int>, а auto e{0}; находится в форме auto e{(list of ints)};, то добавление определенных исключений на основе длины списка вызовет особые ненужные сложности.   -  person    schedule 19.03.2014
comment
Что касается вашего auto d = {0}; // type of d is int -- либо это неправильно, либо реализация GCC. GCC также делает d типа std::initializer_list<int>.   -  person    schedule 19.03.2014
comment
@hvd да, это неправильно, тип `std::initializer_list‹int›! Извини за это! Надеюсь, теперь это имеет смысл!   -  person gnzlbg    schedule 19.03.2014
comment
@hvd Я бы предпочел, чтобы auto e{0,0} было применением оператора запятой, а auto e{{0,0}} выводилось в initializer_list. Просто для согласованности, поскольку decltype(0,0) выводит применение оператора запятой: decltype(0,0) a = 0 и auto a{0,0}; a = 0; в настоящее время даже отдаленно не похожи.   -  person gnzlbg    schedule 19.03.2014
comment
Вы предлагаете что-то с семантикой, которая выглядит очень запутанной. Итак, вы ожидаете найти где-нибудь документ, объясняющий, почему auto a{0,0} не использует оператор запятой? Возможно, они есть в Вавилонской библиотеке.   -  person juanchopanza    schedule 19.03.2014
comment
@juanchopanza вы предполагаете, что семантика стандартизированного решения не сбивает с толку? Я предлагаю что-то последовательное, и я нашел документ, где это обсуждается частично, хотя и не полностью. Кроме того, я почти уверен, что в списке рассылки std.proposals есть и другие подобные документы, а также темы, в которых обсуждается этот вопрос, но я просто не могу их найти. Более того, я знаю, что это было сделано не так, как я предлагаю, по уважительной причине.   -  person gnzlbg    schedule 19.03.2014
comment
@gnzlbg Итак, вы хотели бы, чтобы запятая в auto e = {0,0}; анализировалась как оператор, даже если ее нет в int e[] = {0,0};, даже до того, как auto преобразуется в конкретный тип? Последний не может измениться, это было бы огромной обратной несовместимостью.   -  person    schedule 19.03.2014
comment
@hvd auto e = {0,0} однозначно является initializer_list, auto e{...} IMO несовместим с decltype. Эти два случая являются редкостью для ИМО. Однако в контексте конструкторов, принимающих несколько аргументов, по сравнению с перегрузками, принимающими initializer_list, неожиданное может произойти незаметно (и IMO действительно случается). Я сделал акцент на том, что проблема заключается в том, какой векторный конструктор выбран.   -  person gnzlbg    schedule 19.03.2014
comment
Ах, ладно, я не знал, что вы хотите, чтобы auto e{...} и auto e = {...} выводили по-разному. Я думаю, что это также вызвало бы много путаницы.   -  person    schedule 19.03.2014
comment
возможно, путаница возникает из-за использования {} для обозначения юниформ-инициализации и std::initializer_lists.   -  person gnzlbg    schedule 19.03.2014
comment
@hvd, возможно, сбивает с толку, но в С++ 14 auto e{...} и auto e = {...} выводят по-разному, см. N3922, который был включен в рабочий документ в прошлом месяце.   -  person Jonathan Wakely    schedule 19.03.2014
comment
@gnzlbg, decltype(0,0) a = 0 и auto a{0,0}; a = 0; в настоящее время даже отдаленно не похожи ... и это хорошо. decltype сообщает вам тип выражения, список-инициализаторов — это список выражений, а не одно выражение. Запятые в списках не используют оператор запятой (рассмотрите списки параметров функций, списки аргументов шаблонов, списки ctor-initializer и т. д. и т. д.)   -  person Jonathan Wakely    schedule 19.03.2014
comment
@JonathanWakely спасибо за ссылку на N3922! Я не уверен в decltype/auto в этом случае, поэтому я бы предпочел сосредоточить обсуждение на проблемах с перегрузками конструктора, использующими initializer_list, что и побудило меня задать вопрос. IMO X a{...}; создает X, передавая ... своему конструктору, поэтому очевидным было бы использование X a{ {...} } для передачи одного списка инициализаторов (X a{ {...}, {...} } для передачи двух списков инициализаторов,...). Ярлык X a{...}, где ... выводится как список инициализаторов, кажется, доставляет больше головной боли, чем того стоит.   -  person gnzlbg    schedule 19.03.2014
comment
@JonathanWakely Спасибо, вау. Именно то, что я не мог себе представить, кому-то может понадобиться ошибка, заключается в том, что это пример того, что станет ошибкой. :)   -  person    schedule 19.03.2014
comment
@gnzlbg: Большое спасибо за этот пост, я полностью согласен с вашим предложением. В настоящее время ситуация запутанная, как сообщил Скотт Мейерс в своем блог.   -  person Maggyero    schedule 31.07.2015


Ответы (2)


Вот что сказал по этому поводу Страуструп:

Униформа и универсальность не были задуманы как четвертая альтернатива. Он был разработан как синтаксис инициализации и, к сожалению, [не] подходил для использования со всем устаревшим кодом, особенно с vector. Если бы я разработал vector сегодня, вам пришлось бы сказать что-то вроде vector<int> {Count{9}};, чтобы получить счет.

И в ответ на вопрос Является ли проблема вектором или синтаксисом {}-init?

Это векторный дизайн: если бы я разработал vector сегодня, вам пришлось бы сказать что-то вроде vector<int> {Count{9}};, чтобы получить счет.

Более общая проблема состоит в том, что наличие нескольких семантически разных аргументов одного и того же типа в конечном итоге приводит к путанице, особенно если они могут встречаться прилагательными. Например:

vector<int> v(7,2);    // 7 (a count) element with the value 2
person Jonathan Wakely    schedule 19.03.2014

(На самом деле это не ответ, а просто обсуждение того, о чем я думал по этому вопросу.)

Я думаю, что я хотел бы, чтобы компиляторы выдавали предупреждение в случаях двусмысленности, советуя разработчику использовать либо ({ }) (если им нужен список_инициализаторов), либо ( ), если они этого не делают. С дополнительным предупреждением, если MostVexingParse представляет собой риск! - возможно, порекомендовать (( )), чтобы избежать этого?

(Эта следующая «история», вероятно, не основана на правильной исторической хронологии разработки этой функции, но это то, как я понимаю текущие правила в компиляторе.)

В начале у нас были конструкторы:

type t (...);

Затем у нас возникла идея разрешить { предоставлять литеральную коллекцию для использования в конструкторах (но также и в других местах).

type t ( {...} );

... вместе с новым типом initializer_list в конструкторах, чтобы соответствовать этому.

Затем нам разрешили заменить ( ) на { }, чтобы избежать самого неприятного синтаксического анализа:

type t { ... };
type t { {...} };

Все идет нормально. Чистые расширения языка.

Наконец, «спорное» дополнение заключается в том, что когда компилятор видит { ... } (как конструктор), он сначала пытается переписать его как ({ ... }) (вызывая список инициализаторов, если он существует), прежде чем вернуться к ( ... ). Я думаю, что я бы предпочел, чтобы оба варианта считались одинаково хорошими и чтобы было предупреждение или ошибка, если оба варианта возможны.

person Aaron McDaid    schedule 19.03.2014