隐式生成与用户提供的默认构造函数为何行为不同?
示例代码
#include <memory> #include <cstddef> class A { public: A() : top_(data_) {} private: // 注:移除alignas会让GCC输出中不再出现`rep stosq` 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{}; // 使用B b = B()或B b; 输出结果不变 b.f(); } void g() { C c{}; // C c = C()输出不变,但C c; 会变化 c.f(); }
Clang编译后的汇编输出
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
问题背景
在实际场景中,A是栈分配器,初始化时调用memset会造成严重的性能浪费。GCC、Clang和MSVC都存在此现象,说明这是符合C++标准的行为,而非编译器实现问题。即使将data_替换为union,也无法改变这一情况。
核心疑问:为何B和C的初始化行为不同?
cppreference中有相关描述:
……它(隐式生成的构造函数)的效果与拥有空函数体和空初始化列表的用户定义构造函数相同。
在值初始化期间,拥有用户提供的空构造函数的类类型可能与拥有隐式生成的默认构造函数的类类型被区别对待。
虽然描述较为模糊,但推测这与用户提供的构造函数会使类不再是平凡可构造的有关,现需对此进行澄清。
原因解析
这一差异的根源在于C++标准中值初始化的规则,以及类的平凡可构造性对初始化行为的影响:
类B的构造函数是用户提供的空构造函数:
执行B b{}(值初始化)时,用户提供的构造函数会被调用。该构造函数的空函数体意味着它只会调用成员a的构造函数(即A的构造函数),而不会对B的内存做额外清零操作。A的构造函数仅初始化top_指针,不会触碰data_数组,因此编译后的代码中没有memset或类似指令。类C的构造函数是默认生成的(
= default):C的默认构造函数是隐式生成的,由于A的构造函数是非平凡的(用户提供了构造函数),C的默认构造函数也非平凡。根据C++标准,对具有隐式生成默认构造函数的类进行值初始化时,如果该类不是平凡可构造且平凡可析构的,编译器会先将对象的内存清零,再调用构造函数。因此汇编中会出现memset调用,在调用A的构造函数前先清零C的整个内存区域(包括A的data_数组)。平凡可构造性的关键影响:
用户提供的构造函数会直接让类失去平凡可构造性,但= default的构造函数是否平凡取决于其成员的构造函数。由于A的构造函数非平凡,C的默认构造函数也非平凡,触发了值初始化时的内存清零逻辑。而B的用户提供构造函数被视为完全控制了初始化过程,编译器不会额外插入清零步骤。
内容的提问来源于stack exchange,提问作者ARentalTV

