Modern C++ Features – Default Initializers for Member Variables

Contents

One of the less discussed but nevertheless useful features in C++11 is the possibility to provide initializers for class members right in the class definition.

How it works

You can simply provide a default value by writing an initializer after its declaration in the class definition. Both braced and equal initializers are allowed – they are therefore calle brace-or-equal-initializer by the C++ standard:

class X {
  int i = 4;
  int j {5};
};

Those initializers then are implicitly used in any constructor unless you specifically initialize the members in the member initializer list of that constructor, e.g.

X::X(int) : i{22}
{}

In this case, `i` gets initialized with 22, while `j` gets initialized with 5, because it was not explicitly mentioned in that member initializer list.

The brace-or-equal-initializer for members is not restricted to literals, you can as well call functions or use other expressions.

Providing default values

Obviously, this feature works best for member variables that are most times initialized with the same default value or a value that can be determined by a static function. If you have classes that don’t need complicated logic in their constructors, providing brace-or-equal-initializer for their member variables can make writing constructors for them unnecessary altogether.

In the above example, the compiler generated default constructor initializes `i` with 4 and `j` with 5. If you have additional constructors and still want a default constructor, declare it as defaulted and you are done:

class X {
  int i = 4;
  int j {5};
public:
  X(int a) : i{a} {}  //initializes with a and 5
  X() = default;      //initializes with 4 and 5
};

Especially when you have several constructors and all or most of them initialize their members with the same value, brace-or-equal-initializer for members can not only save you some typing in the member initializer list of those constructors, it makes any element of those list a very explicit hint that there is an initialization which is not the default.

Consider using brace-or-equal-initializers to provide default values for members and make member initializer lists in constructors less verbose and more explicit.

Avoiding uninitialized members

If class members are neither mentioned in a constructor’s member initializer list nor have a brace-or-equal-initializer, then they get default-initialized. That means, that for class types the default constructor is called, but for any other types like enums or built in types like int, double, pointers, no initialization happens at all.

This applies for each element of array, and, as a corollary, it applies for plain old data classes as well, as their default constructor in turn default-initializes all of their members. No initialization means your member variables possibly contain garbage values.

For example, have a look at this little class:

struct Trivial { 
  int k; 
private: 
  int l; 
};

struct Problematic {
 vector<int> vi;
 int u;
 Trivial t;

 Problematic() = default;
};

A default constructed object of type `Problematic` is, in fact, problematic, because neither its member `u` nor the members of `t` will be initialized with any meaningful value. Only `vi` has a nontrivial default constructor and therefore will be initialized correctly to represent an empty vector.

Some compilers are friendly enough to zero-initialize your members anyways in debug mode, so you won’t see any surprises when toying around with a new class. However, once you switch on optimizations, that zero-initialization is among the first things to go and you are in for a debug-session in optimized code to find the origins of those funny values and access violations.

Luckily by now we know how to guard ourselves against this problem: by providing a brace-or-equal-initializer for those members. The usual thing one wants to do with them is zero-initialize them, and there is a one-size-fits-all approach for that:

struct NotProblematic {
 vector<int> vi = {};
 int u = {};
 Trivial t = {};

 NotProblematic() = default;
};

Here you see that I provided empty braces for all elements. This is simply using uniform initialization for them, initializing `u` and all members of `t` with 0, and calling the default constructor for `vi`.

Prefer to provide brace-or-equal-initializers for all trivial members on nontrivial classes. Use empty braces to zero-initialize them if no other default values is sensible.

The initializer for the vector member in the example is not necessary, since the default constructor will be called anyways as described above. However, it won’t hurt either, since it will not generate any different code. If you want to provide initializers for nontrivial members is up to you. If you are not sure whether a member is trivial or not, I’d prefer to err on the safe side and provide the potentially unnecessary initializer.

Counterindication: Members of trivial classes

Providing brace-or-equal-initializers makes a class nontrivial. This may incur a slight performance overhead which, unless I am dealing with a performance bottleneck, I would accept in favor of the increased safety and simplicity.

However, this also prohibits aggregate initialization. If we stick to the old rule of initializing a variable when we declare it and if we provide initializers for trivial members of nontrivial classes like described above, then a object of trivial type will never stay uninitialized:

void foo() {
  NotProblematic np;     //np.t is trivial but initialized, see above
  Trivial ok = {42, 77}; //initialized, too.
  Trivial nope;          //uninitialized - don't do this!
  Trivial okAgain = {};  //ok, initialized to {0, 0}
}

Conclusion

brace-or-equal-initializers can help to avoid problems with uninitialized members and simplify the member initializer lists of our constructors up to the point where we can simply default the constructors. This feature plays well together with the topic of my next post: Inherited and delegating constructors.

Previous Post
Next Post

5 Comments


  1. You say that a default member initializer causes the class to be non-trivial which may incur a slight performance overhead. Can you elaborate on that? What is the performance impact?

    Reply

    1. Trivial objects may be uninitialized, e.g. if you look at nope in the last code snippet, the compiler does nothing but reserve memory for the object on the stack. Its members are not initialized with any specific value. As soon as we add default initializers, the object and its affected members have to be initialized which costs some time at runtime. We usually want this as it makes our program more predictable, safer etc, but it also costs a bit of time which we have to keep in mind in performance critical code.

      Reply

  2. Aggregate initialization was restored for classes with brace-or-equal-initializers as of C++14.

    Reply

  3. I was under the assumption, that is no explicit initialization is performed, the default constructor is called – always. and for integral type, that is initialization to zero. How did you come up with the idea, that the values might skip initialization completely? Is it part of the standard?

    Reply

    1. See http://eel.is/c++draft/dcl.init:

      12 “If no initializer is specified for an object, the object is default-initialized. […]”
      7 “To default-initialize an object of type T means: […]”
      7.3 -> no initialization is performed for types that are not of class type.

      Reply

Leave a Reply

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