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?
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:
- 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.