Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trying to Understand the Initialization of C++ STL Containers

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:

  1. I believe the compile error is caused by the fact that 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?
  2. Now for all the vectors from V0 to V6, I understand V0, V1, V4. Can someone help me understand V2, V3, V5 and V6?
  3. I don't quite understand 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? :-)

like image 909
Peter Lee Avatar asked Jan 02 '20 06:01

Peter Lee


People also ask

What is initialization list in C?

The initializer list is used to directly initialize data members of a class. An initializer list starts after the constructor name and its parameters.

How are STL containers implemented in C++?

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.

What is STL containers in C++?

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.


1 Answers

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.


queues 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.

like image 90
Nicol Bolas Avatar answered Oct 24 '22 17:10

Nicol Bolas