Modern C++ Features – Uniform Initialization and initializer_list

With this post I’ll start a series on new C++ features, where new means C++11 and up. I usually won’t go into too much technical details of the features, because especially the more commonly known C++11 features have been covered elsewhere. Instead, I will try to shed some light on how these features can be used to make code more simple and readable. Today I’ll begin with uniform initialization syntax and `initializer_list`s.

Uniform Initialization

In C++03, initialization of variables has been different for different kinds of variables, and sometimes it was not even possible. With C++11 we got so-called *uniform initialization*, which attempts to make the whole topic a bit easier for developers.

Uniform initialization is pretty simple: you can initialize practically everything with arguments in curly braces. The compiler then will do just the right thing.

Consider this C++03 code:

struct POD { int i; float f; };

class C {
  POD p;
  int iarr[3];
  double d;
public:
  C() : d(3.14) {
    p.i=2; p.f=22.34;
    for (unsigned i = 0; i < 3; ++i) iarr[i] = i;
  }
};

class D {
public:
  D(C const&, POD const&) {}
};

int main() {
  C c; 
  D d(C(), POD()); 
  int i = int();
  POD p = {i, 6.5};
  POD* pp = new POD();
  pp->i = 4;
  pp->f = 22.1;
  float pf* = new float[2];
  pf[0] = 1.2f;
  pf[1] = 2.3f;
}

There are several problems and one outright error connected to initialization in this code, that uniform initialization in C++11 can fix. Let’s look at all the initializations one after another.

Initializing POD Class Members

In C’s constructor only the member variable `d` gets initialized, which is not very clean. We should strive to initialize every member variable in the initializer list, because if we don’t plain old data types remain uninitialized and contain garbage values, not even zeroes.

The problem is, that in C++03, aggregates can not be initialized in the initialization list but instead need to get their values assigned later. With uniform initialization they can:

class C {
  //...
  C()
    : p{2, 44.11}
    , iarr{0, 1, 2}
    , d{22.34}
  {}
};

 

As you can see, the `double` member which is no aggregate can be initialized with curly braces as well.

Calling Constructors and the Most Vexing Parse

In C++03 there is a problem called the “most vexing parse”, and it originates from the fact that parenthesis are used as well for function declarations, function calls and initializations.

The simplest example is the “explicit call of a default constructor” which is not possible in C++03. If a call with one argument looks like `C c(22);`, then with zero arguments it should look like `C c();`. However, the compiler parses this as a declaration of a function `c`, returning a `C` and taking no arguments. We therefore had to rely on an implicit call of the default constructor, writing `C c;`

Line 21 is not very different – we *think* we are constructing a `D`, named `d`, with two constructor arguments being a default-constructed `C` and a default-constructed `POD`. But the compiler reads this as a declaration of a function called `d`, returning a `D`, taking as first argument a function returning a `C` with no parameters and as second a function returning a `POD` with no arguments.

With uniform initialization, the curly braces are not ambiguous, and we can use any initialization that would be victim to the most vexing parse otherwise:

C c{};
D d{C{}, POD{}};

Of course we can call any constructor with curly braces.

Initializing Plain Old Data and Aggregates

Given what I have written about uniform initialization by now, the initialization of plain old data and aggregates won’t come as a surprise, so I’ll just dump the remaining lines of `main` with uniform initialization for completeness:

int i{};
POD p{i, 6.5};
POD* pp = new POD{4, 22.1};
float* pf = new float[2]{1.2, 2.3};

As you can see, there is no more need to “adjust” values after the initialization of an aggregate, since the can get all the needed values through uniform initialization.

Construction of Function Parameters

You can even construct function parameters on the fly, without stating their type, as long as the parameter types stay unambiguous. For example, there is only one constructor of `D`, so the following line is not ambiguous:

D d{{}, {}};

It simply says “default construct the first and second argument” – and the compiler can figure which type those arguments should have.

std::initializer_list

In standardese, the curly braces with the list of elements are called braced-init-list, and the list of elements itself is called initializer-list. In some cases it would come handy if we just could construct an object of class type with such a list, e.g. if we could initialize a container with a list of its elements, just like an array:

std::vector<std::string> names{ "Braum", "Janna", "Leona", "Sona" };

And indeed a way to achieve this has been included into the C++11 standard which makes the above line valid C++.

C++11 introduces a new type `std::initializer_list<T>`. Objects of that class are container proxies with forward iterators and a size to a temporary array.

They are primarily meant for such list initializations but can also be used for function calls, e.g. `f({1, 2, 5});` calls a function `f` that has an `initializer_list<int>` parameter.

std::initializer_list and Uniform Initialization

Mostly, the two features I presented play very well together, for example if you want to initialize a map you can use an initializer-list of braced-init-lists of the key value pairs:

std::map<std::string, int> scores{ 
  {"Alex", 522}, {"Pumu", 423}, {"Kitten", 956} 
};

Here, the type of the pairs is clear and the compiler will deduce, that `{“Alex”, 522}` in fact means `std::pair<std::string const, int>{“Alex”, 522}`.

But sometimes, `initializer_list` constructors can compete against the other constructors. The most prominent example is `std::vector<int>`’s constructor for a size and a single value argument that the vector should be filled with:

std::vector<int> aDozenOfFives{12, 5};

The intention is clear from the naming, the programmer would have expected to get a vector that has twelve elements, each having the value 5. However, the compiler sees this as a call to `vector`’s `initializer_list` constructor, producing a vector with two elements, with the values 12 and 5.

To get the desired behavior in cases like this, the old school syntax with parenthesis has to be employed: `std::vector<int> aDozenOfFoves(12, 5);`

Guidelines

Uniform initialization abstracts away details that are in most cases unnecessary to know, like if an object is an aggregate or of class type. The uniformity enhances readability.

Use uniform intialization where possible.

`std::initializer_list` should be treated carefully. If you write a class that can really benefit from it, use it, but be aware of the interference with normal constructors. In addition, avoid using `std::initializer_list`s as standalone objects. There is not much use to it, and there are some problems with auto type deduction.

Use `std::initializer_list` sparsely and with care.

The last guideline for today is about the omission of function argument types. You will have noticed that the construction of the `D` object becomes pretty obscure if we use only the braced-init-lists, especially if they are empty. A forest of curly braces does not enhance readability.

On the other hand, when constructing the map of scores, the purpose of the initializers were pretty clear, there was no need to explicitly state the type of pairs that were constructed.

Omit the types of brace-initialized function arguments only if they are very clear from the context. Prefer readability and explicitness over brevity.

Previous Post
Next Post

10 Comments


  1. Which way a more correct?

    Object * ptr = {};
    vs
    Object* ptr = nullptr;

    Reply

    1. I’d say both achieve the same outcome, but nullptr is what people expect to see, so I consider that more readable

      Reply

      1. Thank you, but I want to know a more exactly info.

        If we have a both achieve the same outcome, than could you please to show context from Standard C++14.

        For example MS says explicitly:
        Zero initialization is performed at different times:

        During value initialization, for scalar types and POD class types that are initialized by using empty braces.

        See more:
        https://docs.microsoft.com/en-us/cpp/cpp/initializers?view=msvc-160

        Reply

        1. for the = {} you can start here: https://eel.is/c++draft/dcl.init#general-16.1
          which then leads to list-initialization (copy-list-initialization, to be precise): https://eel.is/c++draft/dcl.init#list
          and then we fall through all the different cases to see that the pointer is value-initialized: https://eel.is/c++draft/dcl.init#list-3.11
          and that means for pointers, that it is zero-initialized: https://eel.is/c++draft/dcl.init#general-8.3
          which means it is initialized with a converted zero (0): https://eel.is/c++draft/dcl.init#general-6.1

          in the end, that is a null pointer constant, same as nullptr_t: http://eel.is/c++draft/conv.ptr

          Reply

          1. I found a little bit different opinion:
            nullptr is only to tell the user what is the initial value, besides that neither nullptr nor braces are not necessary.





  2. Uniform initialization abstracts away details that are in most cases unnecessary to know, like if an object is an aggregate or of class type.

    Not always. Consider the following:

    struct MaryPoppins
    {
    int Mary;
    double Poppins;
    };

    std::vector myVector{{1, 1.0}, {2, 2.0}, {3, 3.0}};

    // std::array should also probably work the same way right?
    std::array myArray{{1, 1.0}, {2, 2.0}, {3, 3.0}}; // WRONG! Won’t compile.

    // Correct way. Side-effect of the fact that std::array is a POD.
    std::array myArray{{{1, 1.0}, {2, 2.0}, {3, 3.0}}}; // Note the extra braces.

    Reply

    1. Yes, perhaps I should rephrase that part – uniform initialization levels some of the differences between aggregates and classes but not all.

      I agree that this is not like we would like to have it, but I think it is perfectly clear that an aggregate std::array can not possibly be initialized the same way as a C-array, because it encapsulates the C-array inside an aggregate, therefore the extra curly braces are needed. If you’d put an std::array inside another aggregate you’d need yet another layer of braces:

      struct ManyPoppins {
      std::array thePoppinses;
      };

      ManyPoppins mp{{{{1, 1.0}, {2, 2.0}, {3, 3.0}}}};

      Also, if you had a class that contains a vector of elements and should be initialized with such a vector, you would have to use the same double layer of braces as for a std::array:

      class X {
      public:
      X(std::vector);
      }

      X x{{{1, 1.0}, {2, 2.0}, {3, 3.0}}};

      So the difference here is not really between aggregates and classes but between the levels of nesting (conceptual or real) in std::array (2 levels), C-arrays and std::vector (1 level each)

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *