Рассмотрим следующий код:
#include <initializer_list>
#include <utility>
template<class T>
struct test
{
test(const std::pair<T, T> &)
{}
};
template<class T>
test(std::initializer_list<T>) -> test<T>;
int main()
{
test t{{1, 2}};
}
Хотелось бы понять, почему компилируется этот трюк с initializer_list. Похоже, что сначала {1, 2} рассматривается как initializer_list, но затем это переинтерпретированный как инициализация списка pair.
Что именно здесь происходит, шаг за шагом?





Он компилируется, потому что так работают руководства по выводу шаблонов классов.
Направляющие вывода - это конструкторы типа гипотетический. Их на самом деле не существует. Их единственная цель - определить, как выводить параметры шаблона класса.
Как только вывод сделан, фактический код C++ вступает во владение конкретным экземпляром test. Таким образом, вместо test t{{1, 2}}; компилятор ведет себя так, как если бы вы сказали test<int> t{{1, 2}};.
У test<int> есть конструктор, который принимает pair<int, int>, который может соответствовать значениям в фигурном-инициализирующем списке, поэтому вызывается именно он.
Частично это сделано для того, чтобы агрегаты могли участвовать в выводе аргументов шаблона класса. У агрегатов нет конструкторов, предоставляемых пользователем, поэтому, если бы руководства по дедукции были ограничены только реальными конструкторами, агрегаты не могли бы работать.
Итак, у нас есть это руководство по выводам шаблонов классов для std::array:
template <class T, class... U>
array(T, U...) -> array<T, 1 + sizeof...(U)>;
Это позволяет std::array arr = {2, 4, 6, 7}; работать. Он выводит как аргумент шаблона, так и длину из руководства, но поскольку руководство не является конструктором, array остается агрегатом.
С вашим руководством по дедукции мы получаем то, что эквивалентно:
test<int> t{{1, 2}};
Это работает из-за инициализации списка, раздел dcl.init.listp3.7, который говорит:
Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution ([over.match], [over.match.list]). If a narrowing conversion (see below) is required to convert any of the arguments, the program is ill-formed. [ Example:
struct S { S(std::initializer_list<double>); // #1 S(std::initializer_list<int>); // #2 S(); // #3 // ... }; S s1 = { 1.0, 2.0, 3.0 }; // invoke #1 S s2 = { 1, 2, 3 }; // invoke #2 S s3 = { }; // invoke #3— end example ] [ Example:
struct Map { Map(std::initializer_list<std::pair<std::string,int>>); }; Map ship = {{"Sophie",14}, {"Surprise",28}};— end example ] [ Example:
struct S { // no initializer-list constructors S(int, double, double); // #1 S(); // #2 // ... }; S s1 = { 1, 2, 3.0 }; // OK: invoke #1 S s2 { 1.0, 2, 3 }; // error: narrowing S s3 { }; // OK: invoke #2— end example ]
В противном случае у нас есть невыведенный контекст
Я так понимаю, что это «гипотетический» конструктор. Что меня смутило, так это то, что конструкция {1, 2} обрабатывалась по-разному на разных этапах этого процесса ...