In code like this:
#include <iostream>
#include <initializer_list>
#include <string>
struct A
{
A() { std::cout << "2" << std::endl; }
A(int a) { std::cout << "0" << std::endl; }
A(std::initializer_list<std::string> s) { std::cout << "3" << std::endl; }
A(std::initializer_list<int> l) { std::cout << "1" << std::endl; }
};
int main()
{
A a1{{}};
}
Why does it call std::initializer_list<int>
specification of constructor?
It'll generate ambiguity compilation error if we define, for example, constructor with std::initializer_list<double>
. What are the rules of such construction and why is it so specific about std::initializer_list
with number as template argument?
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.