std::initializer_list constructor

If a class has an initializer list constructor, then {whatever goes here} means to pass {whatevergoeshere} as argument to the present constructors (if there are no initializer list constructors, then whatever goes here are passed as arguments).

So let's simplify the setting and ignore the other constructors, because apparently the compilers don't care about them

void f(std::initializer_list<std::string> s);
void f(std::initializer_list<int> l); 

For f({{}}) we have this rule

Otherwise, if the parameter type is std​::​initializer_­list and all the elements of the initializer list can be implicitly converted to X, the implicit conversion sequence is the worst conversion necessary to convert an element of the list to X, or if the initializer list has no elements, the identity conversion. This conversion can be a user-defined conversion even in the context of a call to an initializer-list constructor.

Here we have a single element {} and it needs a user defined conversion to initialize std::string and no conversion (identity) for int. Therefore, int is chosen.

For f({{{}}}) the element is {{}}. Can it be converted to int? The rule is

  • if the initializer list has one element that is not itself an initializer list, the implicit conversion sequence is the one required to convert the element to the parameter type
  • ...
  • In all cases other than those enumerated above, no conversion is possible.

Can it be converted to std::string? Yes, because it has an initializer list constructor that has a std::initializer_list<char> init parameter. Therefore, std::string is chosen this time.

The difference to A a3({}) is that in such a case, it's not list initialization, but a "normal" initialization with a {} argument (note that one less nesting because of the missing outer braces). Here our two f-functions are called with {}. And since both lists have no elements, for both we have identity conversions and therefore an ambiguity.

The compiler in this case will also consider f(int) and get a tie with the other two functions. But a tie-breaker would apply that declares the int -parameter worse than the initializer_list parameters. So you have a partial order {int} < {initializer_list<string>, initializer_list<int>}, which is the reason for ambiguity, as the best group of conversion sequences does not contain a single candidate, but two.

{} to a scalar type (such as int, double, char*, etc.) is the identity conversion.

{} to a class type other than a specialization of std::initializer_list (e.g., std::string) is a user-defined conversion.

The former beats the latter.