I am trying to understand the initialization of C++ STL containers. Here is the nightmare I got:
vector<int> V0 ({ 10, 20 }); // ok - initialized with 2 elements
vector<int> V1 = { 10, 20 }; // ok - initialized with 2 elements
vector<int> V2 = {{ 10, 20 }}; // ok - initialized with 2 elements
vector<int> V3 = {{ 10 }, 20 }; // ok - initialized with 2 elements
vector<int> V4 { 10, 20 }; // ok - initialized with 2 elements
vector<int> V5 {{ 10, 20 }}; // ok - initialized with 2 elements
vector<int> V6 {{ 10 }, 20 }; // ok - initialized with 2 elements
queue<int> Q0 ({ 10, 20 }); // ok - initialized with 2 elements
// queue<int> Q1 = { 10, 20 }; // compile error
// queue<int> Q2 = {{ 10, 20 }}; // compile error
// queue<int> Q3 = {{ 10 }, 20 }; // compile error
// queue<int> Q4 { 10, 20 }; // compile error
queue<int> Q5 {{ 10, 20 }}; // ok - initialized with 2 elements
// queue<int> Q6 {{ 10 }, 20 }; // compile error
We are talking about C++11.
I did some research, and here are my questions:
queue<T>
's initializer_list
constructor is missing. See vector<T>
: http://www.cplusplus.com/reference/vector/vector/vector/ and queue<T>
:
http://www.cplusplus.com/reference/queue/queue/queue/ am I right?V0
to V6
, I understand V0
, V1
, V4
. Can someone help me understand V2
, V3
, V5
and V6
?Q0
or Q5
. Can someone help me?I am also reading Mike Lui's article: Initialization in C++ is Seriously Bonkers. I'd like to share it with you guys, but is there a quick way to help me understand this nightmare? :-)
The initializer list is used to directly initialize data members of a class. An initializer list starts after the constructor name and its parameters.
A container is a holder object that stores a collection of other objects (its elements). They are implemented as class templates, which allows great flexibility in the types supported as elements.
The Standard Template Library (STL) is a set of C++ template classes to provide common programming data structures and functions such as lists, stacks, arrays, etc. It is a library of container classes, algorithms, and iterators. It is a generalized library and so, its components are parameterized.
There is nothing "nightmarish" about any of that. You simply need to read what you've written. More specifically, you have to work through the rules from the outside in, systematically.
vector<int> V0 ({ 10, 20 });
Calls a vector
constructor (that's what the ()
mean), passing it a single braced-init-list. Therefore, it will pick a constructor that takes one value, but only the constructor whose first parameter can be initialized by a 2-element braced-init-list containing integers. Such as the initializer_list<int>
constructor that vector<int>
contains.
vector<int> V1 = { 10, 20 };
List initialization (that's what happens when you initialize a thing with a braced-init-list directly). Under list-initialization rules, all constructors of the type which take a single initializer_list
parameter are considered first. The system attempts to initializer these constructors with the braced-init-list directly; if it can succeed with one of the candidate constructors, then that constructor is called.
Obviously, you can initialize an initializer_list<int>
from a 2-element braced-init-list of integers. And this is the only initializer_list
constructor in vector
, so it gets called.
vector<int> V2 = {{ 10, 20 }};
Still list initialization. Again, initializer_list
constructors which match the values in the braced-init-list are considered. However, the "value" in the braced-init-list is itself another braced-init-list. int
cannot be initialized from a 2-element braced-init-list, so the initializer_list<int>
cannot be initialized by {{10, 20}}
.
Since no initializer_list
constructor can be used, all constructors are considered under normal function overload resolution rules. In this case, the members of the (outer) braced-init-list are considered arguments to the constructors of the type. There is only one value in the outer braced-init-list, so only constructors which can be called with one argument are considered.
The system will attempt to initialize the first parameter of all such constructors with the inner braced-init-list. And there is a constructor whose parameter can be initialized by a 2-element braced-init-list of integers. Namely, the initializer_list<int>
constructor. That is, while initializer_list<int>
can't be initialized by {{10, 20}}
, it can be initialized by just {10, 20}
.
vector<int> V3 = {{ 10 }, 20 };
Again, still list initialization. So again, we first attempt to apply the full braced-init-list to any initializer_list
constructors of the type. Can initializer_list<int>
be initialized from a braced-init-list of {{10}, 20}
? Yes. So that's what happens.
Why does this work? Because any type T
which is copyable/moveable can always be initialized from a braced-init-list containing some value of that type. That is, if T t = some_val;
works, then so too does T t = {some_val};
(unless T
has an initializer_list
constructor that takes T
, which would be decidedly weird). And if T t = {some_val};
works, then so too does initializer_list<T> il = {{some_val}};
.
vector<int> V4 { 10, 20 }; // ok - initialized with 2 elements
vector<int> V5 {{ 10, 20 }}; // ok - initialized with 2 elements
vector<int> V6 {{ 10 }, 20 }; // ok - initialized with 2 elements
These are identical to 1, 2, and 3. List initialization is often called "uniform initialization" because there is (almost) no difference between using the braced-init-list directly and using = braced-init-list
. The only times there is a difference is if an explicit constructor is selected or if you're using auto
with a single value in the braced-init-list.
queue
s initializer_list
constructor is not "missing". It doesn't exist on purpose because queue
is not a container. It is a container adapter type. It stores a container and adapts the container's interface to be restricted to queue operations: push, pop, and peek.
So none of those are supposed to work.
queue<int> Q0 ({ 10, 20 });
This calls a constructor of queue<int>
using regular old overload resolution, just like V0
. The only difference is that the constructor it selects is the one that takes the container type for the queue. Since you didn't specify a container, it uses the default: std::deque<int>
, which can be constructed from a braced-init-list of two integers.
queue<int> Q5 {{ 10, 20 }};
Same situation as V2
. There are no initializer_list constructors on queue
, so it acts exactly like Q0
: using overload resolution to select a constructor whose argument can take a braced-init-list of 2 integers.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With