Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Vector initialization with double curly braces: std::string vs int

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?

like image 376
bolov Avatar asked Oct 10 '17 11:10

bolov


2 Answers

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.

like image 162
Barry Avatar answered Oct 21 '22 00:10

Barry


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.

like image 21
StoryTeller - Unslander Monica Avatar answered Oct 21 '22 01:10

StoryTeller - Unslander Monica