Рассмотрим следующий минимальный пример:
#include <iostream>
struct X {
X() { std::cout << "Default-ctor" << std::endl; }
X(std::initializer_list<int> l) {
std::cout << "Ilist-ctor: " << l.size() << std::endl;
}
};
int main() {
X a{};
X b({}); // reads as construct from {}
X c{{}}; // reads as construct from {0}
X d{{{}}}; // reads as construct from what?
// X e{{{{}}}}; // fails as expected
}
У меня нет вопросов по а, б и в, все достаточно понятно
Но я не могу понять, почему д работает
Что означает эта дополнительная пара фигурных скобок в d? Я посмотрел стандарт С++ 20, но не могу легко найти ответ. И clang, и gcc согласны с этим кодом, так что это я что-то упускаю
@chris Я запустил его с C++ 14 и -fno-elide-constructors
, и конструкторы не вызывались.
Хороший трюк, чтобы получить информацию о том, что делает компилятор, состоит в том, чтобы скомпилировать все ошибки:
-Weverything
. Давайте посмотрим на результат здесь (только для d
):
9.cpp:16:6: warning: constructor call from initializer list is incompatible with C++98
[-Wc++98-compat]
X d{{{}}}; // reads as construct from what?
^~~~~~
X::X(std::initializer_list)
называется.
9.cpp:16:8: warning: scalar initialized from empty initializer list is incompatible with
C++98 [-Wc++98-compat]
X d{{{}}}; // reads as construct from what?
^~
Скаляр (int
) инициализируется во внутреннем {}
. Итак, у нас есть X d{{0}}
.
9.cpp:16:7: warning: initialization of initializer_list object is incompatible with
C++98 [-Wc++98-compat]
X d{{{}}}; // reads as construct from what?
^~~~
5 warnings generated.
std::initializer_list
инициализируется из {0}
. Итак, у нас есть X d{std::initializer_list<int>{0}};
!
Это показывает нам все, что нам нужно. Дополнительная скобка предназначена для построения списка инициализаторов.
Примечание. Если вы хотите добавить дополнительные скобки, вы можете вызвать конструктор копирования/перемещения (или исключить его), но компиляторы C++ не будут делать это неявно, чтобы предотвратить ошибки:
X d{X{{{}}}}; // OK
X e{{{{}}}}; // ERROR
Думал, что просто проиллюстрирую:
X d{ { {} }};
| | |
construct an | |
`X` from ... an initializer_list |
containing... int{}
Правила для инициализации списка заключаются в том, чтобы найти конструктор initializer_list<T>
и использовать его, если это вообще возможно, в противном случае... перечислить конструкторы и выполнить обычные действия.
С X{{}}
это инициализация списка: самые внешние {}
— это initializer_list
, и они содержат один элемент: {}
, то есть 0
. Достаточно прямолинейно (хотя и загадочно).
Но с X{{{}}}
это больше не работает, используя самый внешний {}
в качестве initializer_list
, потому что вы не можете инициализировать int
из {{}}
. Поэтому мы возвращаемся к использованию конструкторов. Теперь один из конструкторов принимает initializer_list
, так что это похоже на начало, за исключением того, что мы уже сняли один слой фигурных скобок.
Вот почему, например, vector<int>{{1, 2, 3}}
тоже работает, а не только vector<int>{1, 2, 3}
. Но вроде... нет.
Спасибо, я уже принял первый ответ, но это также отличное объяснение. Кстати, я нашел еще более интересный пример безумия скобок: std::vector<std::string> vs = {{{{{}}}}}; Здесь, следуя вашему объяснению, строка построена из {{}} и вектора из {{s}} с унифицированной инициализацией {v}, вау...
Я не уверен, но это может быть копирование/перемещение из другого временного объекта, инициализированного с помощью внутренних фигурных скобок (и, конечно, на самом деле это не будет копирование/перемещение из-за правил С++ 17).