c++11initializer-listuniform-initialization

How does nested list-initialization forward its arguments?


In the initialization of a vector of pairs

  std::vector<std::pair<int, std::string>> foo{{1.0, "one"}, {2.0, "two"}};

how am I supposed to interpret the construction of foo? As I understand it,

  1. The constructor is called with braced initialization syntax so the overload vector( std::initializer_list<T> init, const Allocator& alloc = Allocator() ); is strongly preferred and selected
  2. The template parameter of std::initializer_list<T> is deduced to std::pair<int, std::string>
  3. Each element of foo is a std::pair. However, std::pair has no overload accepting std::initializer_list.

I am not so sure about step 3. I know the inner braces can't be interpreted as std::initializer_list since they are heterogenous. What mechanism in the standard is actually constructing each element of foo? I suspect the answer has something to do with forwarding the arguments in the inner braces to the overload template< class U1, class U2 pair( U1&& x, U2&& y ); but I don't know what this is called.

EDIT:

I figure a simpler way to ask the same question would be: When one does

std::map<int, std::string> m = { // nested list-initialization
           {1, "a"},
           {2, {'a', 'b', 'c'} },
           {3, s1}

as shown in the cppreference example, where in the standard does it say that {1, "a"}, {2, {'a', 'b', 'c'} }, and {3, s1} each get forwarded to the constructor for std::pair<int, std::string>?


Solution

  • Usually, expressions are analyzed inside-out: The inner expressions have types and these types then decide which meaning the outer operators have and which functions are to be called.

    But initializer lists are not expressions, and have no type. Therefore, inside-out does not work. Special overload resolution rules are needed to account for initializer lists.

    The first rule is: If there are constructors with a single parameter that is some initializer_list<T>, then in a first round of overload resolution only such constructors are considered (over.match.list).

    The second rule is: For each initializer_list<T> candidate (there could be more than one of them per class, with different T each), it is checked that each initializer can be converted to T, and only those candidates remain where this works out (over.ics.list).

    This second rule is basically, where the initializer-lists-have-no-type hurdle is taken and inside-out analysis is resumed.

    Once overload resolution has decided that a particular initializer_list<T> constructor should be used, copy-initialization is used to initialize the elements of type T of the initializer list.