What language rules permit C++11 to deduce that this is an initializer_list of pairs?
Asked Answered
F

2

9

In C++11, it seems like it's legal to initialize a std::map<std::string, int> as follows:

std::map<std::string, int> myMap = {
     { "One",   1 },
     { "Two",   2 },
     { "Three", 3 }
};

Intuitively, this makes sense - the brace-enclosed initializer is a list of pairs of strings, and std::map<std::string, int>::value_type is std::pair<std::string, int> (possibly with some const qualifications.

However, I'm not sure I understand how the typing works here. If we eliminate the variable declaration here and just have the brace-enclosed initializer, the compiler wouldn't know that it was looking at a std::initializer_list<std::pair<std::string, int>> because it wouldn't know that the braced pairs represented std::pairs. Therefore, it seems as though the compiler is somehow deferring the act of assigning a type to the brace-enclosed initializer until it has enough type information from the std::map constructor to realize that the nested braces are for pairs. I don't remember anything like this happening in C++03; to the best of my knowledge, the type of an expression never depended on its context.

What language rules permit this code to compile correctly and for the compiler to determine what type to use for the initializer list? I'm hoping for answers with specific references to the C++11 spec, since it's really interesting that this works!

Thanks!

Fireball answered 13/7, 2014 at 0:41 Comment(3)
Brace-enclosed lists in C++03 were already very specific to the initializer context. There's no significant difference here from a C++03 nested aggregate (maybe std::pair<std::string, int> myArray[]) initialized using braces, except that the type isn't directly named, it's inferred from the available constructors.Allwein
BTW, implicit conversion operators (member operator T()) are the other place where the context determines the type of an expression (and is used for overload resolution).Allwein
braced-init-list is not an expression and has no type. There is no deduction, only overload resolution among the initializer-list constructors (of which there is only one)Leone
F
9

In the expression

std::map<std::string, int> myMap = {
     { "One",   1 },
     { "Two",   2 },
     { "Three", 3 }
};

on the right side you have a braced-init-list where each element is also a braced-init-list. The first thing that happens is that the initializer list constructor of std::map is considered.

map(initializer_list<value_type>,
    const Compare& = Compare(),
    const Allocator& = Allocator());

map<K, V>::value_type is a typedef for pair<const K, V>, in this case pair<const string, int>. The inner braced-init-lists can be successfully converted to map::value_type because std::pair has a constructor that takes references to its two constituent types, and std::string has an implicit conversion constructor that takes a char const *.

Thus the initializer list constructor of std::map is viable, and the construction can happen from the nested braced-init-lists.

The relevant standardese is present in §13.3.1.7/1 [over.match.list]

When objects of non-aggregate class type T are list-initialized (8.5.4), overload resolution selects the constructor in two phases:
— Initially, the candidate functions are the initializer-list constructors (8.5.4) of the class T and the argument list consists of the initializer list as a single argument.
— If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list.

The first bullet is what causes the initializer_list constructor of map to be selected for the outer braced-init-list, while the second bullet results in the selection of the correct pair constructor for the inner braced-init-lists.

Feudalize answered 13/7, 2014 at 2:25 Comment(0)
S
2

This is list-initialization. The rules are found in §8.5.4[dcl.init.list]/p3 of the standard:

List-initialization of an object or reference of type T is defined as follows:

  • If the initializer list has no elements and T is a class type with a default constructor, the object is value-initialized.
  • Otherwise, if T is an aggregate, aggregate initialization is performed (8.5.1). [example omitted]
  • Otherwise, if T is a specialization of std::initializer_list<E>, an initializer_list object is constructed as described below and used to initialize the object according to the rules for initialization of an object from a class of the same type (8.5).
  • Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution (13.3, 13.3.1.7). If a narrowing conversion (see below) is required to convert any of the arguments, the program is ill-formed.
  • [example and remainder of the rules omitted]

Note that overload resolution will prefer std::initializer_list constructors in these cases (§13.3.1.7 [over.match.list]).

Thus when the compiler sees an braced list used to initialize a object of a non-aggregate, non-std::initializer_list class type, it will perform overload resolution to select the appropriate constructor, preferring the initializer_list constructor if a viable one exists (as it does for std::map).

Shoreless answered 13/7, 2014 at 2:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.