Table of Contents

With Modern C++ and each revision of the Standard, we get more comfortable ways to initialize data members. There’s non-static data member initialization (from C++11) and inline variables (for static members since C++17).

In this blog post, you’ll learn how to use the syntax and how it has changed over the years. We’ll go from C++11, through C++14, and C++17 until C++20.

Updated in July 2022: added more examples, use cases, and C++20 features.

Initialisation of Data members  

Before C++11, if you had a class member, you could only initialize it with a default value through the initialization list in a constructor.

// pre C++11 class:
struct SimpleType {
    int field;
    std::string name;
    
    SimpleType() : field(0), name("Hello World") { }
}

Since C++11, the syntax has been improved, and you can initialize field and name in the place of the declaration:

// since C++11:
struct SimpleType {
    int field = 0;                        // works now!
    std::string name { "Hello World "} // alternate way with { }
    
    SimpleType() { }
}

As you can see, the variables get their default value in the place of declaration. There’s no need to set values inside a constructor.

The feature is called non-static data member initialization, or NSDMI in short.

What’s more, since C++17, we can initialise static data members thanks to inline variables:

struct OtherType {
    static const int value = 10;
    static inline std::string className = "Hello Class";
    
    OtherType() { }
}

There’s no need to define className in a corresponding cpp file. The compiler guarantees that all compilation units will see only one definition of the static member. Previously, before C++17, you had to put the definition in one of cpp files.

Please note that for constant integer static fields (value), we could initialize them “in place” even in C++98.

Let’s explore those useful features: NSDMI and inline variables. We’ll see the examples and how the features improved over the years.

NSDMI - Non-static data member initialization  

In short, the compiler performs the initialization of your fields as you’d write it in the constructor initializer list.

SimpleType() : field(0) { }

Let’s see this in detail:

How It works  

With a bit of “machinery,” we can see when the compiler performs the initialization.

Let’s consider the following type:

struct SimpleType
{
    int a { initA() }; 
    std::string b { initB() }; 
    
    // ...
};

The implementation of initA() and initB() functions have side effects and they log extra messages::

int initA() {
    std::cout << "initA() called\n";
    return 1;
}

std::string initB() {
    std::cout << "initB() called\n";
    return "Hello";
}

This allows us to see when the code is called.

For example:

struct SimpleType
{
    int a { initA() }; 
    std::string b { initB() }; 

    SimpleType() { }
    SimpleType(int x) : a(x) { }
};

And the use:

std::cout << "SimpleType t10\n";    
SimpleType t0;
std::cout << "SimpleType t1(10)\n";    
SimpleType t1(10);

The output:

SimpleType t0:
initA() called
initB() called
SimpleType t1(10):
initB() called

t0 is default initialized; therefore, both fields are initialized with their default value.

In the second case, for t1, only one value is default initialized, and the other comes from the constructor parameter.

As you might already guess, the compiler performs the initialization of the fields as if the fields were initialized in a “member initialization list.” So they get the default values before the constructor’s body is invoked.

In other words the compiler expands the code:

int a { initA() }; 
std::string b { initB() }; 

SimpleType() { }
SimpleType(int x) : a(x) { }

into

int a; 
std::string b; 

SimpleType() : a(initA()), b(initB()) { }
SimpleType(int x) : a(x), b(initB())  { }

How about other constructors?

Copy and Move Constructors  

The compiler initializes the fields in all constructors, including copy and move constructors. However, when a copy or move constructor is default, there’s no need to perform that extra initialization.

See the examples:

struct SimpleType
{        
    int a { initA() }; 
    std::string b { initB() };

    SimpleType() { }
    
    SimpleType(const SimpleType& other) {
        std::cout << "copy ctor\n";
        
        a = other.a;
        b = other.b;
    };

};

And the use case:

SimpleType t1;
std::cout << "SimpleType t2 = t1:\n";
SimpleType t2 = t1;

The output:

SimpleType t1:
initA() called
initB() called
SimpleType t2 = t1:
initA() called
initB() called
copy ctor

See code here @Wandbox.

The compiler initialized the fields with their default values in the above example. That’s why it’s better also to use the initializer list inside a copy constructor:

SimpleType(const SimpleType& other) : a(other.a), b(other.b) {
        std::cout << "copy ctor\n";
    };

We get:

SimpleType t1:
initA() called
initB() called
SimpleType t2 = t1:
copy ctor

The same happens if you rely on the copy constructor generated by the compiler:

SimpleType(const SimpleType& other) = default;

You get a similar behavior for the move constructor.

Other forms of NSDMI  

Let’s try some other examples and see all options that we can initialize a data member using NSDMI:

struct S {
    int zero {};       // fine, value initialization
    int a = 10;        // fine, copy initialization    
    double b { 10.5 }; // fine, direct list initialization
    // short c ( 100 );   // err, direct initialization with parens
    int d { zero + a }; // dependency, risky, but fine
    // double e { *mem * 2.0 }; // undefined!
    int* mem = new int(d);
    long arr[4] = { 0, 1, 2, 3 };
    std::array<int, 4> moreNumbers { 10, 20, 30, 40};
    // long arr2[] = { 1, 2 }; // cannot deduce
    // auto f = 1;     // err, type deduction doesn't work
    double g { compute() };

    ~S() { delete mem; }
    double compute() { return a*b; }
};

See @Compiler Explorer.

Here’s the summary:

  • zero uses value initialization, and thus, it will get the value of 0,
  • a uses copy initialization,
  • b uses direct list initialization,
  • c would generate an error as direct initialization with parens is not allowed for NSDMI,
  • d initializes by reading zero and a, but since d appears later in the list of data members, it’s okay, and the order is well defined,
  • e, on the other hand, would have to read from a data member mem, which might not be initialized yet (since it’s further in the declaration order), and thus this behavior is undefined,
  • mem uses a memory allocation which is also acceptable,
  • arr[4] declares and initializes an array, but you need to provide the number of elements as the compiler cannot deduce it (as in arr2),
  • similarly we can use std::array<type, count> for moreNumbers, but we need to provide the count and the type of the array elements,
  • f would also generate an error, as auto type deduction won’t work,
  • g calls a member function to compute the value. The code is valid only when that function calls reads from already initialized data members.

C++14 Updates for aggregates, NSDMI  

Originally, in C++11, if you used default member initialisation then your class couldn’t be an agregate type:

struct Point { float x = 0.0f; float y = 0.0f; };

// won't compile in C++11
Point myPt { 10.0f, 11.0f};

I was unaware of this issue, but Shafik Yaghmour noted that in the comments below the article.

In C++11, spec did not allow aggregate types to have such initialization, but in C++14 this requirement was removed. Link to the StackOverflow question with details

Fortunately, it’s fixed in C++14, so

Point myPt { 10.0f, 11.0f};

Compiles as expected; see @Wandbox

C++20 Updates for bit fields  

Since C++11, the code only considered “regular” fields… but how about bit fields in a class?

class Type {
    int value : 4;
};

This is only a recent change in C++20 that allows you to write:

class Type {
    int value : 4 = 0;
    int second : 4 { 10 };
};

The proposal was accepted into C++20 as Default Bit Field Initialiser for C++20 P0683.

The case with auto  

Since we can declare and initialize a variable inside a class, there’s an interesting question about auto. Can we use it? It seems quite a natural way and would follow the AAA (Almost Always Auto) rule.

You can use auto for static variables:

class Type {
    static inline auto theMeaningOfLife = 42; // int deduced
};

But not as a class non-static member:

class Type {
    auto myField { 0 };   // error
    auto param { 10.5f }; // error  
};

Unfortunately, auto is not supported. For example, in GCC I get

error: non-static data member declared with placeholder 'auto'

While static members are just static variables, and that’s why it’s relatively easy for the compiler to deduce the type, it’s not that easy for regular members. This is mostly because of the possible cyclic dependencies of types and the class layout. If you’re interested in the full story, you can read this great explanation at cor3ntin blog: The case for Auto Non-Static Data Member Initializers | cor3ntin.

The case with CTAD - Class Template Argument Deduction  

Similarly, as with auto we also have limitations with non-static member variables and CTAD:

It works for static variables:

class Type {
    static inline std::vector ints { 1, 2, 3, 4, 5, 6, 7}; // deduced vector<int>
};

But not as a non-static-member:

class Type {
    std::vector ints { 1, 2, 3, 4, 5, 6, 7}; // error!
};

On GCC 10.0 I get

error: 'vector' does not name a type

Advantages of NSDMI  

  • It’s easy to write.
  • You can be sure that each member is initialized correctly.
  • The declaration and the default value are in the same place, so it’s easier to maintain.
  • It’s much easier to conform to the rule that every variable should be initialized.
  • It is beneficial when we have several constructors. Previously, we would have to duplicate the initialization code for members or write a custom method, like InitMembers(), that would be called in the constructors. Now, you can do a default initialization, and the constructors will only do their specific jobs.

Any negative sides of NSDMI?  

On the other hand, the feature has some limitations and inconveniences:

  • Using NSDMI makes a class not trivial, as the default constructor (compiler-generated) has to perform some work to initialize data members.
  • Performance: When you have performance-critical data structures (for example, a Vector3D class), you may want to have an “empty” initialization code. You risk having uninitialized data members, but you might save several CPU instructions.
  • (Only until C++14) NSDMI makes a class non-aggregate in C++11. Thanks, Yehezkel, for mentioning that! This drawback also applies to static variables that we’ll discuss later.
  • They have limitations in the case of auto type deduction and CTAD, so you need to provide the type of the data member explicitly.
  • You cannot use direct initialization with parens, to fix it, you need list initialization or copy initialization syntax for data members.
  • Since the default values are in a header file, any change can require recompiling dependent compilation units. This is not the case if the values are set only in an implementation file.
  • Might be hard to read if you rely on calling member functions or depend on other data members.

Do you see any other issues?

Inline Variables C++17  

So far, we have discussed non-static data members. Do we have any improvements for declaring and initializing static variables in a class?

In C++11/14, you had to define a variable in a corresponding cpp file:

// a header file:
struct OtherType {
    static int classCounter;

    // ...
};

// implementation, cpp file
int OtherType::classCounter = 0;

Fortunately, with C++17, we also got inline variables, which means you can define a static inline variable inside a class without defining them in a cpp file.

// a header file, C++17:
struct OtherType {
    static inline int classCounter = 0;

    // ...
};

One note: before C++17, you could declare and define a constant static integer data member, but since C++17 it’s “extended” to all types (and also mutable) through the inline keyword.

// a header file, C++17:
struct MyClass {
    static const int ImportantValue = 99; // declaration and definition in one place

    // ...
};

The compiler guarantees that there’s precisely one definition of this static variable for all translation units, including the class declaration. Inline variables are still static class variables so that they will be initialized before the main() function is called (You can read more in my separate article What happens to your static variables at the start of the program?).

The feature makes it much easier to develop header-only libraries, as there’s no need to create cpp files for static variables or use some hacks to keep them in a header file.

Here’s the full example at @Wandbox

Summary  

In this article, we reviewed how in-class member initialization changed with Modern C++.

In C++11, we got NSDMI - non-static data member initialization. You can now declare a member variable and init that with a default value. The initialization will happen before each constructor body is called, in the constructor initialization list.

NSDMI improved with C++14 (aggregates) and in C++20 (bit fields are now supported).

The feature is also reflected in C++ Core Guidelines:

C.48: Prefer in-class initializers to member initializers in constructors for constant initializers

Reason: Makes it explicit that the same value is expected to be used in all constructors. Avoids repetition. Avoids maintenance problems. It leads to the shortest and most efficient code. Here’s a “summary” example that combines the features:

What’s more, in C++17, we got inline variables, which means you can declare and initialize a static member without the need to do that in a corresponding cpp file.

Here’s a “summary” example that combines the features:

struct Window
{        
    inline static unsigned int default_width = 1028;
    inline static unsigned int default_height = 768;
    
    unsigned int _width { default_width };
    unsigned int _height { default_height };
    unsigned int _flags : 4 { 0 };
    std::string _title { "Default Window" };

    Window() { }
    Window(std::string title) : _title(std::move(title)) { }
    // ...
};

Play at @Wandbox

For simplicity, default_width and default_height are static variables that can be loaded, for example, from a configuration file, and then be used to initialize a default Window state.

Your Turn

  • Do you use NSDMI in your projects?
  • Do you use static Inline variables as class members?

Even more in a book and a course!  

The topic of data member initialization was so interesting to me that I followed the rabbit hole and investigated many related areas. In summary, I created a book with almost 200 pages where you can learn about special member functions (constructors, destructors, copy, move), and various ways of object initialization, all across C++11 up to C++20.


Data Member Initialization in Modern C++ @Leanpub

Leanpub offers a 60-day refund period!

Buy together with my C++ Lambda Story ebook: Buy C++Lambda Story and Data Members in C++, 14.99$ instead of 29.98$

If you like, you can also take a simplified version of the book and look at my interactive Educative minicourse:

Initializing Data Members: From C++11 till C++20

See here: Initializing Data Members: From C++11 till C++20

17 short lessons, interactive code samples and more!