使用unique_ptr实现pimpl惯用法时声明析构函数仍编译失败问题
问题1:为什么初始代码编译main.cpp会抛出不完整类型错误?
你的思路本身是正确的:将析构函数的定义放到Impl可见的翻译单元,确实可以让析构函数中调用的unique_ptr删除逻辑在完整类型下实例化。但你忽略了C++特殊成员函数的隐式生成规则:
- 当你只显式声明了析构函数,未显式声明构造函数时,编译器会自动隐式生成默认构造、移动构造、移动赋值运算符等特殊成员函数,这些函数默认是
inline的,会在调用它们的翻译单元(也就是你的main.cpp)中直接生成代码。 - 这些隐式生成的函数在操作
_impl成员时,部分场景(比如移动赋值需要销毁旧的_impl、构造异常回溯时销毁已构造的成员)会触发std::default_delete<Impl>::operator()的实例化,而main.cpp中只有Impl的前置声明,属于不完整类型,因此触发了头文件中的static_assert报错。
问题2:为什么把wrapper.cpp的代码加到main.cpp末尾就能解决错误?
C++以翻译单元为单位编译,单个翻译单元是指.cpp文件加上其递归引入的所有头文件的完整内容:
- 当你把
Impl类的定义和Wrapper析构函数的实现放到main.cpp末尾后,整个翻译单元内Impl就变成了完整类型。无论代码中的哪个位置触发std::default_delete<Impl>::operator()的实例化,都能看到完整的Impl定义,自然不会报不完整类型错误。 - 编译器会先处理完整个翻译单元的所有声明和定义,再做模板实例化的类型检查,所以即使
Impl的定义在main函数之后,也不影响前面代码的类型检查逻辑。
问题3:为什么添加构造函数声明可以解决编译错误?
显式添加构造函数声明后,C++的特殊成员函数隐式生成规则会发生变化,刚好避开了在main.cpp中实例化删除逻辑的问题:
- 显式声明构造函数后,编译器不会再自动生成默认构造函数,你需要自行在wrapper.cpp中实现构造函数,构造函数的实现位于
Impl可见的翻译单元,不会有类型不完整的问题。 - 当类中同时存在用户显式声明的构造函数和析构函数时,编译器不会再自动生成移动构造、移动赋值运算符,同时拷贝构造和拷贝赋值运算符会被隐式标记为删除。
- 此时main.cpp中只用到了
Wrapper的构造和析构,这两个函数都是你显式声明的,它们的实现都位于wrapper.cpp中,main.cpp不需要生成任何Wrapper特殊成员函数的代码,也就不会触发std::default_delete<Impl>的实例化,因此不会报错。
补充说明
unique_ptr实现Pimpl的标准写法就是需要在头文件中显式声明构造、析构函数,如果需要支持移动语义也要显式声明移动构造/移动赋值,如果需要禁止拷贝就显式delete拷贝构造/拷贝赋值,所有这些函数的实现都要放到Impl类定义可见的.cpp文件中,才能避免出现不完整类型的编译错误。
内容的提问来源于stack exchange,提问作者Asaf
相关产品推荐
相关产品推荐

