How does nested list-initialization forward its arguments?

328 Views Asked by At

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>?

2

There are 2 best solutions below

5
j6t On BEST ANSWER

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.

2
TonySalimi On

You are confusing two different concepts:

1) initializer lists

initializer_list<T>: that is mainly used for initialization of collections. In this case, all of the members should be of the same type. (not applicable for std::pair)

Example:

std::vector<int> vec {1, 2, 3, 4, 5};

2) Uniform initialization

Uniform initialization: in which the braces are used to construct and initialize some objects like structs, classes (with an appropriate constructor) and basic types (int, char, etc.).

Example:

struct X { int x; std::string s;}; 
X x{1, "Hi"}; // Not an initializer_list here.

Having mentioned that, for initialization of a std::pair with a brace initializer, you will need a constructor that takes two elements, i.e. the first and the second elements, not a std::initializer_list<T>. For example on my machine with VS2015 installed, this constructor looks like this:

template<class _Other1,
    class _Other2,
    class = enable_if_t<is_constructible<_Ty1, _Other1>::value
                    && is_constructible<_Ty2, _Other2>::value>,
    enable_if_t<is_convertible<_Other1, _Ty1>::value
            && is_convertible<_Other2, _Ty2>::value, int> = 0>
    constexpr pair(_Other1&& _Val1, _Other2&& _Val2) // -----> Here the constructor takes 2 input params
        _NOEXCEPT_OP((is_nothrow_constructible<_Ty1, _Other1>::value
            && is_nothrow_constructible<_Ty2, _Other2>::value))
    : first(_STD forward<_Other1>(_Val1)), // ----> initialize the first 
            second(_STD forward<_Other2>(_Val2)) // ----> initialize the second
    {   // construct from moved values
    }