In an answer to this question: Initializing vector<string> with double curly braces
it is shown that
vector<string> v = {{"a", "b"}};
will call the std::vector
constructor with an initializer_list
with one element. So the first (and only) element in the vector will be constructed from {"a", "b"}
. This leads to undefined behavior, but that is beyond the point here.
What I have found is that
std::vector<int> v = {{2, 3}};
Will call std::vector
constructor with an initializer_list
of two elements.
Why is the reason for this difference in behavior?
The rules for list initialization of class types are basically: first, do overload resolution only considering std::initializer_list
constructors and then, if necessary, do overload resolution on all the constructors (this is [over.match.list]).
When initializing a std::initializer_list<E>
from an initializer list, it's as if we materialized a const E[N]
from the N elements in the initializer list (from [dcl.init.list]/5).
For vector<string> v = {{"a", "b"}};
we first try the initializer_list<string>
constructor, which would involve trying to initialize an array of 1 const string
, with the one string
initialized from {"a", "b"}
. This is viable because of the iterator-pair constructor of string
, so we end up with a vector containing one single string (which is UB because we violate the preconditions of that string constructor). This is the easy case.
For vector<int> v = {{2, 3}};
we first try the initializer_list<int>
constructor, which would involve trying to initialize an array of 1 const int
, with the one int
initialized from {2, 3}
. This is not viable.
So then, we redo overload resolution considering all the vector
constructors. Now, we get two viable constructors:
vector(vector&& )
, because when we recursively initialize the parameter there, the initializer list would be {2, 3}
- with which we would try to initialize an array of 2 const int
, which is viable.
vector(std::initializer_list<int> )
, again. This time not from the normal list-initialization world but just direct-initializing the initializer_list
from the same {2, 3}
initializer list, which is viable for the same reasons.
To pick which constructor, we have to go to into [over.ics.list], where the vector(vector&& )
constructor is a user-defined conversion sequence but the vector(initializer_list<int> )
constructor is identity, so it's preferred.
For completeness, vector(vector const&)
is also viable, but we'd prefer the move constructor to the copy constructor for other reasons.
The difference in behavior is due to default parameters. std::vector
has this c'tor:
vector( std::initializer_list<T> init,
const Allocator& alloc = Allocator() );
Note the second argument. {2, 3}
is deduced as std::initializer_list<int>
(no conversion of the initializers is required) and the allocator is defaulted.