Empty list initialization

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:

  1. The optional object may not be “engaged” (may not even contain a vector).
  2. 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?

This entry was posted in programming and tagged , . Bookmark the permalink.

9 Responses to Empty list initialization

  1. anon says:

    So how do you initialize the std::optional with the empty vector? Don’t tell me you really need to do something like std::optional x = vectorT(); ?

    • optional<vector<T>> ov{ in_place };
      

      std::in_place is a tag indicating that we want to create an “engaged” optional “in place” (as if with placement new) using the argument list that follows: empty list in our example.

  2. Etam says:

    “Would you notice this whole problem if I didn’t tell you?”
    No. Thanks for some knowledge 🙂

  3. pavan says:

    Great post. This post useful for all techies. Thanks for this post..!

  4. Nawaz says:

    Great post.

    However, the comment here is wrong:

    vector s( int(), int(i) ); // function with 2 int params

    The first param is not `int`, It is a function type `int (*)()`.

  5. Your article was very interesting for me. Thanks!
    I’m interested in new C++ standard and I see that your blog is a great source of information about it.
    Generally, I think that these new features will make C++ incontestable leader (now it’s only a leader 😉 in the programming language’s area.

  6. Guette31 says:

    You said that proper ways to create a NullableNameList instance with an empty list are:
    NullableNameList p( {} );
    NullableNameList q{ initializer_list{} };

    However, later in the article, you explain the reason why initializer-list constructor does not take precedence over the default constructor in list-initialization for empty initializer list is that the initializer-list constructor selection would be ambiguous if there were several on them.
    That’s exactly what would happen if we used NullableNameList p( {} ).
    Therefore, NullableNameList q{ initializer_list{} } is, in my opinion, the only consistent way to do it .

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.