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


Translate

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;
}

All Answers
  • Translate

    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).


  • Translate

    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.


  • Translate

    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.


  • Translate

    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.


  • Translate

    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?