持有std::unique_ptr至不完全类型的类构造函数定义差异原因
unique_ptr编译差异 以下代码可正常编译:
#include <memory> struct B; struct A1 { A1() = default; ~A1(); std::unique_ptr<B> ptr; }; #if 0 struct A2 { A2(); ~A2(); std::unique_ptr<B> ptr; }; A2::A2() = default; #endif int main() { }
但将#if 0替换为#if 1编译类A2时,GCC(MSVC下结果类似,C17和C20编译结果一致)会报如下错误:
In file included from /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/memory:76, from <source>:1: /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/unique_ptr.h: In instantiation of 'void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = B]': /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/unique_ptr.h:396:17: required from 'std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = B; _Dp = std::default_delete<B>]' <source>:17:1: required from here /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/unique_ptr.h:93:23: error: invalid application of 'sizeof' to incomplete type 'B' 93 | static_assert(sizeof(_Tp)>0, | ^~~~~~~~~~~ ASM generation compiler returned: 1 In file included from /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/memory:76, from <source>:1: /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/unique_ptr.h: In instantiation of 'void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = B]': /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/unique_ptr.h:396:17: required from 'std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = B; _Dp = std::default_delete<B>]' <source>:17:1: required from here /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/unique_ptr.h:93:23: error: invalid application of 'sizeof' to incomplete type 'B' 93 | static_assert(sizeof(_Tp)>0, | ^~~~~~~~~~~ Execution build compiler returned: 1
疑问:A1和A2仅构造函数定义位置不同(均为默认构造),为何编译结果存在差异?
核心在于默认构造函数的定义时机,以及unique_ptr对类型完整性的要求:
A1的情况:A1()是类内默认定义的构造函数。根据C++标准,类内默认定义的特殊成员函数(如默认构造、析构等),其实例化时机被延迟到第一次被调用,或编译器需要生成其代码时。当前代码中main函数并未创建A1的实例,因此A1的默认构造函数和析构函数都不会被实例化。而unique_ptr<B>的析构函数(及其内部default_delete<B>::operator())只有在A1的析构函数被实例化时,才会触发对B类型完整性的检查——此时因未实例化,不会触发sizeof(B)的检查,编译通过。A2的情况:A2()是类外默认定义的构造函数(A2::A2() = default;)。对于类外默认定义的特殊成员函数,编译器会在定义点(即=default;所在行)就确定是否需要生成代码。默认构造函数需要初始化成员ptr(std::unique_ptr<B>),更关键的是:处理类外默认定义的构造函数时,编译器会同步检查该类的析构函数是否需隐式生成,进而触发unique_ptr<B>的析构相关代码的实例化检查——此时B仍是不完整类型,default_delete<B>中的static_assert(sizeof(_Tp)>0)会触发错误,因为sizeof不能作用于不完整类型。
简言之:类内默认构造的实例化被延迟,类外默认构造会在定义点触发成员析构代码的检查,而unique_ptr的析构要求指向的类型必须完整。
内容的提问来源于stack exchange,提问作者wohlstad

