C++17 In Detail

02 March 2020

In-class Member Initialisation: From C++11 to C++20

With modern C++ and with each revision of the standard we get more comfortable way to initialise fields of a class: both static and non-static: there’s non-static data member initialisation (from C++11) and also inline variables (for static members since C++17).

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

Updated: 2nd March 2020.

Initialisation of Data members

Before C++11, if you had a class member, you could only initialise it with a default value through the initialisation 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 is improved, and you can initialise 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 initialisation*, 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() { }
}

Now, 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.

And note, that for constant integer static fields (value), we could initialise 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 initialisation

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

SimpleType() : field(0) { }

Let’s see this in detail:

How It works

With a little “machinery” we can see when the compiler performs the initialisation.

Let’s consider the following type:

struct SimpleType
{
    int a { initA() }; 
    int 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 t0\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 initialised, therefore both of the fields are initialised with their default value.

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

As you might already guess, the compiler performs the initialisation of the fields as if the fields were initialised in a “member initialisation list”. So they get the default values before the body of the constructor 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 performs the initialisation of the fields in all constructors, including copy and move constructors. However, when a copy or move constructor is default, then there’s no need to perform that extra initialisation.

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.

In the above example, the compiler initialised the fields with their default values. That’s why it’s better to also use the initialiser 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 default copy constructor generated by the compiler:

SimpleType(const SimpleType& other) = default;

The same thing happens for a move constructor.

Advantages of NSDMI

  • Easy to write
  • You are sure that each member is initialised correctly.
    • declaration and the default value is in the same place
  • Especially useful when we have several constructors.
    • Previously we would have to duplicate initialisation code for members or write a custom method like InitMembers() that would be called in constructors.
    • Now, you can do a default initialisation and constructors will only do its specific jobs…

Any negative sides of NSDMI?

It’s hard to come up with drawbacks, but let’s try:

  • Performance: when you have performance-critical data structures (for example a Vector3D class) you may want to have “empty” initialisation code. You risk having uninitialized data members, but you will save several instructions.
  • Making class non-aggregate in C++11, but not in C++14. See the section about C++14 changes.
  • Since the default values are in a header file, then any change can cause the need to recompile dependent compilation units. This is not the case if the values are set only in an implementation file. Thanks Yehezkel for mentioning that in comments! This drawback also applies to static variables that we'll discuss later.

Do you see any other issues?

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 not aware 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 initialisation, 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 {
    unsigned int value : 4;
};

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

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

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

Inline Variables C++17

So far, we talked about non-static data members. Do we have any improvements for declaring and initialising 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, that means you can define a static inline variable inside a class, without the need to define them in a cpp file.

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

    // ...
};

The compiler guarantees that there’s precisely one definition of this static variable for all translation units that include the declaration of the class. Inline variables are still static class variables so that they will be initialised 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

The case with auto

Since we can declare and initialise 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. Mostly because of a possibility of 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

Compiler Support

Feature GCC Clang Visual Studio
Non-static data member initializers N2756 4.7 3.0 VS 2013
Default Bit Field Initialiser for C++20 P0683 8 6 not yet
Inline Variables C++17 P0386 7 3.9 VS 2017 15.5

Sorry for a little interruption in the flow :)
I've prepared a little bonus if you're interested in Modern C++, check it out here:

Summary

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

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

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

What’s more in C++17 we got inline variables, which means you can declare and initialise 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 might be loaded, for example, from a configuration file, and then be used to initialise a default Window state.

Your Turn

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

Do you use it in your code?

If you want to get additional C++ resources, exlusive articles, early access content, private Discord server and weekly curated news, check out my Patreon website: (see all benefits):

© 2017, Bartlomiej Filipek, Blogger platform
Disclaimer: Any opinions expressed herein are in no way representative of those of my employers. All data and information provided on this site is for informational purposes only. I try to write complete and accurate articles, but the web-site will not be liable for any errors, omissions, or delays in this information or any losses, injuries, or damages arising from its display or use.
This site contains ads or referral links, which provide me with a commission. Thank you for your understanding.