c++constructorinitializationlanguage-lawyer

Why is implicitly-generated constructor different from user-provided one?


Given the following code (available on godbolt):

#include <memory>
#include <cstddef>

class A {
  public:
    A() : top_(data_) {}
  private:
    // Note: removing alignas removes the `rep stosq` from gcc output
    alignas(std::max_align_t) std::byte data_[1024];
    std::byte* top_;
};

class B {
public:
    B() {};
    void f();
private:
    A a;
};

class C {
public:
    C() = default;
    void f();
private:
    A a;
};

void f() {
    B b{}; // using B b = B() or B b; does not change the output
    b.f();
}

void g() {
   C c{}; // C c = C() does not change the output, C c; does
   c.f();
}

The two functions compile into the following (with clang, see godbolt for other compilers, all generating a rep stos instruction in place of the memset):

f():
        sub     rsp, 1032
        lea     rdi, [rsp + 4]
        call    B::f()@PLT
        add     rsp, 1032
        ret

g():
        push    rbx
        sub     rsp, 1040
        lea     rbx, [rsp + 12]
        mov     edx, 1028
        mov     rdi, rbx
        xor     esi, esi
        call    memset@PLT
        mov     rdi, rbx
        call    C::f()@PLT
        add     rsp, 1040
        pop     rbx
        ret

In my case, A is a stack allocator, so memset-ing at creation is quite wasteful. This happens on GCC, Clang, and MSVC, so there must be some standard clause I'm missing. Even substituting data_ with a union doesn't change that.

Why are B and C initialized differently? cppreference says:

... and it [the implicitly-generated constructor] has the same effect as a user-defined constructor with empty body and empty initializer list.

It further states that

class types with an empty user-provided constructor may get treated differently than those with an implicitly-defined default constructor during value initialization

Though that is somewhat ambiguous, I thought it might refer to the fact that user-provided constructors make the class no longer trivially constructible.

Could someone provide clarification into this please?


Solution

  • tldr; B's ctor is user-provided because it is not defaulted on its first declaration while C's ctor is not user-provided. This results in object c being first zero initialized which is why we see some extra assembly code for c as explained below.


    The behavior of the program can be understood using dcl.init.

    First note that both B b{}; and C c{}; are list-initialization which has the following effect:

    1. List-initialization of an object or reference of type cv T is defined as follows:
    • 3.1
    • 3.2
    • 3.3
    • 3.4
    • 3.5 Otherwise, Otherwise, if the initializer list has no elements and T is a class type with a default constructor, the object is value-initialized.

    Which means both the objects will be value initialized. So we move onto value-initialization:

    To value-initialize an object of type T means:

    • If T is a (possibly cv-qualified) class type ([class]), then let C be the constructor selected to default-initialize the object, if any. If C is not user-provided, the object is first zero-initialized. In all cases, the object is then default-initialized.

    Note the emphasis on user-provided. In particular, from dcl.fct.def.defaut we see that only the ctor B::B() is user-provided because it is not default in its first declaration.

    A function is user-provided if it is user-declared and not explicitly defaulted or deleted on its first declaration.

    Thus the ctor C::C()=default is not user-provided and the object c will first be zero-initialized as opposed to the object b. The object b will not be zero initialized.