Why does C++ require a user-provided default constructor to default-construct a const object?

C++

C++ Problem Overview


The C++ standard (section 8.5) says:

> If a program calls for the default initialization of an object of a const-qualified type T, T shall be a class type with a user-provided default constructor.

Why? I can't think of any reason why a user-provided constructor is required in this case.

struct B{
  B():x(42){}
  int doSomeStuff() const{return x;}
  int x;
};

struct A{
  A(){}//other than "because the standard says so", why is this line required?

  B b;//not required for this example, just to illustrate
      //how this situation isn't totally useless
};

int main(){
  const A a;
}

C++ Solutions


Solution 1 - C++

The reason is that if the class doesn't have a user-defined constructor, then it can be POD, and the POD class is not initialized by default. So if you declare a const object of POD which is uninitialized, what use of it? So I think the Standard enforces this rule so that the object can actually be useful.

struct POD
{
  int i;
};

POD p1; //uninitialized - but don't worry we can assign some value later on!
p1.i = 10; //assign some value later on!

POD p2 = POD(); //initialized

const POD p3 = POD(); //initialized 

const POD p4; //uninitialized  - error - as we cannot change it later on!

But if you make the class a non-POD:

struct nonPOD_A
{
    nonPOD_A() {} //this makes non-POD
};

nonPOD_A a1; //initialized 
const nonPOD_A a2; //initialized 

Note the difference between POD and non-POD.

User-defined constructor is one way to make the class non-POD. There are several ways you can do that.

struct nonPOD_B
{
    virtual void f() {} //virtual function make it non-POD
};

nonPOD_B b1; //initialized 
const nonPOD_B b2; //initialized 

Notice nonPOD_B doesn't defined user-defined constructor. Compile it. It will compile:

And comment the virtual function, then it gives error, as expected:


Well, I think, you misunderstood the passage. It first says this (§8.5/9):

>If no initializer is specified for an object, and the object is of (possibly cv-qualified) non-POD class type (or array thereof), the object shall be default-initialized; [...]

It talks about non-POD class possibly cv-qualified type. That is, the non-POD object shall be default-initialized if there is no initializer specified. And what is default-initialized? For non-POD, the spec says (§8.5/5),

> To default-initialize an object of type T means:
— if T is a non-POD class type (clause 9), the default constructor for T is called (and the initialization is ill-formed if T has no accessible default constructor);

It simply talks about default constructor of T, whether its user-defined or compiler-generated is irrelevant.

If you're clear up to this, then understand what the spec next says ((§8.5/9),

>[...]; if the object is of const-qualified type, the underlying class type shall have a user-declared default constructor.

So this text implies, the program will be ill-formed if the object is of const-qualified POD type, and there is no initializer specified (because POD are not default initialized):

POD p1; //uninitialized - can be useful - hence allowed
const POD p2; //uninitialized - never useful  - hence not allowed - error

By the way, this compiles fine, because its non-POD, and can be default-initialized.

Solution 2 - C++

Pure speculation on my part, but consider that other types have a similar restriction, too:

int main()
{
    const int i; // invalid
}

So not only is this rule consistent, but it also (recursively) prevents unitialized const (sub)objects:

struct X {
    int j;
};
struct A {
    int i;
    X x;
}

int main()
{
    const A a; // a.i and a.x.j in unitialized states!
}

As for the other side of the question (allowing it for types with a default constructor), I think the idea is that a type with a user-provided default constructor is supposed to always be in some sensible state after construction. Note that the rules as they are allow for the following:

struct A {
    explicit
    A(int i): initialized(true), i(i) {} // valued constructor

    A(): initialized(false) {}

    bool initialized;
    int i;
};

const A a; // class invariant set up for the object
           // yet we didn't pay the cost of initializing a.i

Then perhaps we could formulate a rule like 'at least one member must be sensibly initialized in a user-provided default constructor', but that's way too much time spent trying to protect against Murphy. C++ tends to trust the programmer on certain points.

Solution 3 - C++

This was considered a defect (against all versions of the standard) and it was resolved by Core Working Group (CWG) Defect 253. The new wording for the standard states in http://eel.is/c++draft/dcl.init#7

> 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 with at least one non-static data member, exactly one variant member has a default member initializer, > * if T is not a union, for each anonymous union member with at least one non-static data member (if any), exactly one non-static data > member has a default member initializer, and > * each potentially constructed base class of T is const-default-constructible. > > 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.

This wording essentially means that the obvious code works. If you initialize all of your bases and members, you can say A const a; regardless of how or if you spell any constructors.

struct A {
};
A const a;

gcc has accepted this since 4.6.4. clang has accepted this since 3.9.0. Visual Studio also accepts this (at least in 2017, not sure if sooner).

Solution 4 - C++

I was watching Timur Doumler's talk at Meeting C++ 2018 and I finally realised why the standard requires a user-provided constructor here, not merely a user-declared one. It has to do with the rules for value initialisation.

Consider two classes: A has a user-declared constructor, B has a user-provided constructor:

struct A {
    int x;
    A() = default;
};
struct B {
    int x;
    B() {}
};

At first glance, you might think these two constructors will behave the same. But see how value initialisation behaves differently, while only default initialisation behaves the same:

  • A a; is default initialisation: the member int x is uninitialised.
  • B b; is default initialisation: the member int x is uninitialised.
  • A a{}; is value initialisation: the member int x is zero-initialised.
  • B b{}; is value initialisation: the member int x is uninitialised.

Now see what happens when we add const:

  • const A a; is default initialisation: this is ill-formed due to the rule quoted in the question.
  • const B b; is default initialisation: the member int x is uninitialised.
  • const A a{}; is value initialisation: the member int x is zero-initialised.
  • const B b{}; is value initialisation: the member int x is uninitialised.

An uninitialised const scalar (e.g. the int x member) would be useless: writing to it is ill-formed (because it's const) and reading from it is UB (because it holds an indeterminate value). So this rule prevents you from creating such a thing, by forcing you to either add an initialiser or opt-in to the dangerous behaviour by adding a user-provided constructor.

I think it would be nice to have an attribute like [[uninitialized]] to tell the compiler when you're intentionally not initialising an object. Then we wouldn't be forced to make our class not trivially default constructible to get around this corner case. This attribute has actually been proposed, but just like all the other standard attributes, it does not mandate any normative behaviour, being merely a hint to the compiler.

Solution 5 - C++

Congratulations, you've invented a case in which there need not be any user defined constructor for the const declaration with no initializer to make sense.

Now can you come up with a reasonable re-wording of the rule that covers your case but still makes the cases that should be illegal illegal? Is it less than 5 or 6 paragraphs? Is it easy and obvious how it should be applied in any situation?

I posit that coming up with a rule that allows the declaration you created to make sense is really hard, and making sure that the rule can be applied in a way that makes sense to people when reading code is even harder. I would prefer a somewhat restrictive rule that was the right thing to do in most cases to a very nuanced and complex rule that was difficult to understand and apply.

The question is, is there a compelling reason the rule should be more complex? Is there some code that would otherwise be very difficult to write or understand that can be written much more simply if the rule is more complex?

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionKaruView Question on Stackoverflow
Solution 1 - C++NawazView Answer on Stackoverflow
Solution 2 - C++Luc DantonView Answer on Stackoverflow
Solution 3 - C++David StoneView Answer on Stackoverflow
Solution 4 - C++OktalistView Answer on Stackoverflow
Solution 5 - C++OmnifariousView Answer on Stackoverflow