C++11 allows in-class initialization of non-static and non-const members. What changed?

Joseph Mansfield picture Joseph Mansfield · Dec 1, 2012 · Viewed 93.1k times · Source

Before C++11, we could only perform in-class initialization on static const members of integral or enumeration type. Stroustrup discusses this in his C++ FAQ, giving the following example:

class Y {
  const int c3 = 7;           // error: not static
  static int c4 = 7;          // error: not const
  static const float c5 = 7;  // error: not integral
};

And the following reasoning:

So why do these inconvenient restrictions exist? A class is typically declared in a header file and a header file is typically included into many translation units. However, to avoid complicated linker rules, C++ requires that every object has a unique definition. That rule would be broken if C++ allowed in-class definition of entities that needed to be stored in memory as objects.

However, C++11 relaxes these restrictions, allowing in-class initialization of non-static members (§12.6.2/8):

In a non-delegating constructor, if a given non-static data member or base class is not designated by a mem-initializer-id (including the case where there is no mem-initializer-list because the constructor has no ctor-initializer) and the entity is not a virtual base class of an abstract class (10.4), then

  • if the entity is a non-static data member that has a brace-or-equal-initializer, the entity is initialized as specified in 8.5;
  • otherwise, if the entity is a variant member (9.5), no initialization is performed;
  • otherwise, the entity is default-initialized (8.5).

Section 9.4.2 also allows in-class initialization of non-const static members if they are marked with the constexpr specifier.

So what happened to the reasons for the restrictions we had in C++03? Do we just simply accept the "complicated linker rules" or has something else changed that makes this easier to implement?

Answer

Jerry Coffin picture Jerry Coffin · Dec 1, 2012

The short answer is that they kept the linker about the same, at the expense of making the compiler still more complicated than previously.

I.e., instead of this resulting in multiple definitions for the linker to sort out, it still only results in one definition, and the compiler has to sort it out.

It also leads to somewhat more complex rules for the programmer to keep sorted out as well, but it's mostly simple enough that it's not a big deal. The extra rules come in when you have two different initializers specified for a single member:

class X { 
    int a = 1234;
public:
    X() = default;
    X(int z) : a(z) {}
};

Now, the extra rules at this point deal with what value is used to initialize a when you use the non-default constructor. The answer to that is fairly simple: if you use a constructor that doesn't specify any other value, then the 1234 would be used to initialize a -- but if you use a constructor that specifies some other value, then the 1234 is basically ignored.

For example:

#include <iostream>

class X { 
    int a = 1234;
public:
    X() = default;
    X(int z) : a(z) {}

    friend std::ostream &operator<<(std::ostream &os, X const &x) { 
        return os << x.a;
    }
};

int main() { 
    X x;
    X y{5678};

    std::cout << x << "\n" << y;
    return 0;
}

Result:

1234
5678