[Inspired by CppQuiz #264]
Take this code snippet:
struct C { int i; }; const C c; ...
It fails to compile in gcc, with:
error: ‘const struct C’ has no user-provided default constructor and the implicitly-defined constructor does not initialize ‘int C::i’
Clang and icc give similar error messages. MSVC does agree to compile it, but somewhat reluctantly:
warning C4269: ‘c’: ‘const’ automatic data initialized with compiler generated default constructor produces unreliable results
If you remove the const qualifier, everything builds fine. What’s the deal?
Rationale
An uninitialized object that is also constant would not be able to be populated with meaningful values later – and so is a strong indication of a coding error. The C++ standard made an exception to its’ usual philosophy and tried to stop this particular bullet from hitting your foot:
If a program calls for the default-initialization of an object of a const-qualified type T, T shall be a const-default-constructible class type or array thereof.
A class type T is const-default-constructible if default-initialization of T would invoke a user-provided constructor of T (not inherited from a base class) or if
each direct non-variant non-static data member M of T has a default member initializer or, if M is of class type X (or array thereof), X is const-default-constructible,
if T is a union …,
if T is not a union …,
Limitations
1. First and most obvious, the compiler does not try to check whether the user provided ctor actually does everything it should, or anything at all. This builds fine:
struct C { C() {}; int i; }; const C c;
Perhaps the ctor contents could have been checked (most compilers already know enough to generate warnings for uninitialized members), but the current standard doesn’t require it. To appease the compiler, it is enough the user supplies any ctor.
2. Currently there are ways – or rather spec loopholes? – to still use the same compiler-generated constructor for const initialization.
struct C {int i;}; const C c1 = C(); const C c2 {};
– both are considered value initialization, distinct from the default initialization referred in this part of the standard.
3. Somewhat surprisingly, while this fails:
struct C { C() = default; int i; }; const C c;
taking the ‘ = default’ out of the class declaration makes the program valid!
struct C { C(); int i; }; C::C() = default; const C c;
While in this toy example the difference seems negligible, typically the ctor implementation does not appear in all translation units that use C’s declaration. Thus, the ctor implementation – and in particular whether it’s default or not – is invisible to the compiler, and the standard does not require it to take decisions based on an implementation it can’t see.
Standard Bug (?)
Take a closer look at the aforementioned rationale:
An uninitialized object that is also constant would not be able to be populated with meaningful values later
Sure about that?
struct C { volatile int i; }; const C c; ...
Beyond the various ways in which these well-meaning limitations can be bypassed, if the uninitialized member is volatile – the rationale is plain wrong. The code openly asserts that this uninitialized member can be modified anywhere else, even for a const object.
I did come across mentions of this, and one can hope that sometime soon (23?) the standard would make volatile an exception – but I personally feel C++ would have been better off leaving this niche alone. This particular limitation probably caused more head scratching than it saved.