Did you already try using the new initialization syntax with STL containers?

vector<string> va {"three", "element", "vector"}; vector<string> vb {"vector"}; vector<string> vc {};

This works as you might have expected:

assert (va.size() == 3); assert (vb.size() == 1); assert (vc.size() == 0);

The above initialization works because the constructs use the new initializer-list constructor. Well, not really…

Actually, the first two initializations render a call to initializer-list constructor, which is declared as:

template <typename T> vector<T>::vector(initializer_list<T>);

The third renders value-initialization, which in our case means calling vector ’s default constructor. But does this matter, given that the final results are what we expected? It doesn’t matter for std::vector because the semantics of its default constructor are exactly same as these that you would expect of the zero-size initializer-list, but it did matter for us during the design of std::optional (now accepted into C++14).

The problem

One of our design goals was for the following initialization to be possible:

// NOT POSSIBLE: xoptional<vector<string>> oa = {"one", "two"}; // 2-elem list xoptional<vector<string>> ob = {"one"}; // 1-elem list xoptional<vector<string>> oc = {}; // 0-elem list xoptional<vector<string>> od; // no list

Don’t worry about this “ x ”. I only added it in order not to suggest that such things are possible with std::optional . Note that for this type there are two “null” states:

The optional object may not be “engaged” (may not even contain a vector). The optional object may contain a vector that is empty.

Such design often makes sense. The disengaged optional object may represent the state that we did not read the list from the file yet. The engaged optional object containing an empty vector may indicate that we have already read the file and found no records therein.

Let’s imagine we want to implement a type with similar behaviour. We will give it two constructors: the default one, and the list-initializer constructor:

class NullableNameList { optional<vector<string>> data; public: NullableNameList(); // no list NullableNameList(initializer_list<string>); // with list // ... };

Unfortunately (for us — but perhaps fortunately to others), the following initializations

NullableNameList l = {}; NullableNameList m{};

pick the default constructor. Surprised or not? These are the rules of C++ initialization. If you know this, you might want to try the following initialization construct:

NullableNameList n{ {} };

But it won’t work the way one might expect either. It is equivalent to:

NullableNameList n{ string{} }; // 1-elem list

The only way (that I know) you can create a proper empty list is the following:

NullableNameList p( {} ); // parentheses obey different rules NullableNameList q{ initializer_list<string>{} };

Why are these rules so weird?

The new list-initialization feature is often called “uniform initialization,” and people are often outraged when they compare the name with situation as the one described here, or the following one:

vector<int> v{10, 20}; // 2-elem vector vector<int> w(10, 20); // 10-elem vector

This is a misunderstanding of the word “uniform” here. “Uniform” means that whenever list-initialization is used in any context (like initializing members in constructor, initializing a temporary, an automatic object, a dynamically allocated array, etc.) it has always the same meaning. In contrast, initialization in C++03 was not uniform because parentheses meant different things in different contexts:

MyType::MyType() : i() {} // call default constructor int i(); // declare a function vector<int> s( int(), int(i) ); // function with 2 params: // int(*)() and int

But why does initializer-list constructor not take precedence over the default constructor in list-initialization for empty initializer list? There is one good reason. There is no way to deduce the T for parameter initializer_list<T> . Consider that our type had two initializer list constructors:

class MyType { MyType(initializer_list<int>); MyType(initializer_list<string>); // ... };

Which one would we pick? This is somewhat similar to the question of deducing the type of a in:

auto a = {};

Well, I am exaggerating: for a type with one initializer-list constructor the selection would not be ambiguous; but there was a strong motivation for having a syntax to explicitly force the value-initialization in all initialization contexts.

Advice

My conclusion from the above story is this piece of advice. When you provide an initializer-list constructor for your class, make sure that you also provide a default constructor with the same semantics as though you were initializing with a zero-size initializer_list . This way you will spare your users many surprises. STL containers do that. Would you notice this whole problem if I didn’t tell you?