当 使用 Pimpl 方式 时,在 实现文件 中定义特殊成员函数
先看一段代码// widget.hpp #pragma once #include memory class Widget { public: Widget(); Widget::~Widget() default; Widget(Widget rhs) noexcept; Widget operator(Widget rhs) noexcept; Widget(const Widget rhs) delete; Widget operator(const Widget rhs) delete; void interface(); private: struct Impl; std::unique_ptrImpl pImpl; };// widget.cpp#include widget.hpp #include iostream #include memory #include string #include utility struct Widget::Impl { std::string value{UserClassObject}; void concreteInterface() { std::cout value std::endl; } }; Widget::Widget() : pImpl(std::make_uniqueImpl()) { } Widget::Widget(Widget rhs) noexcept default; Widget Widget::operator(Widget rhs) noexcept default; void Widget::interface() { pImpl-concreteInterface(); }// client.cpp#include widget.hpp #include utility int main() { Widget w1; Widget w2 std::move(w1); Widget w3; w3 std::move(w2); w3.interface(); return 0; }编译g -stdc17 -Wall -Wextra -pedantic widget.cpp client.cpp -o widget_demo编译错误信息如下In file included from client.cpp:5878:widget.hpp:10:5: error: extra qualification Widget:: on member Widget [-fpermissive]10 | Widget::~Widget() default;| ^~~~~~In file included from C:/mingw64/lib/gcc/x86_64-w64-mingw32/15.2.0/include/c/memory:80,from widget.hpp:3:C:/mingw64/lib/gcc/x86_64-w64-mingw32/15.2.0/include/c/bits/unique_ptr.h: In instantiation of void std::default_delete_Tp::operator()(_Tp*) const [with _Tp Widget::Impl]:C:/mingw64/lib/gcc/x86_64-w64-mingw32/15.2.0/include/c/bits/unique_ptr.h:399:17: required from std::unique_ptr_Tp, _Dp::~unique_ptr() [with _Tp Widget::Impl; _Dp std::default_deleteWidget::Impl]399 | get_deleter()(std::move(__ptr));| ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~widget.hpp:10:5: required from here10 | Widget::~Widget() default;| ^~~~~~C:/mingw64/lib/gcc/x86_64-w64-mingw32/15.2.0/include/c/bits/unique_ptr.h:91:23: error: invalid application of sizeof to incomplete type Widget::Impl91 | static_assert(sizeof(_Tp)0,| ^~~~~~~~~~~是什么原因造成的对于析构函数 ~Widget(); 这问题是由于生成w1析构时的目标代码报错。在此时析构函数被调用而在定义使用std::unique_ptr的Widget类中 我们并没有声明析构函数因为我们不需要在Widget的析构函数内写任何代码。依据编译器自动生成特殊成员函数(参考条款17)的普通规则编译器为我们生成了一个析构函数。在那个自动生成的析构函数中编译器插入代码调用Widget的数据成员pImpl的析构函数。pImpl是一个 std::unique_ptrWidget::Impl ,即一个使用默认deleter的std::unique_ptr。默认deleter是一个函数对std::unique_ptr里面的原生指针调用delete。然而在调用delete之前编译器通常会让默认deleter先使用C11的static_assert来确保原生指针指向的类型不是imcomplete type(staticassert编译时候检查assert运行时检查---译者注)。当编译器生成Widget w的析构函数时调用的static_assert检查就会失败导致出现了错误信息。在w被销毁时这些错误信息才会出现因为与编译器生成的其他特殊成员函数一样Widget的析构函数也是inline的。出错指向w被创建的那一行因为该行创建了w1导致后来(w1出作用域时)w被隐性销毁。为了修复这个问题你需要确保在生成销毁 std::unique_ptrWidget::Impl 的代码时Widget::Impl是完整的类型。当它的定义被编译器看到时它就是完整类型了。而Widget::Impl在widget.cpp中被定义。所以编译成功的关键在于让编译器只在widget.cpp内在widget::Impl被定义之后看到Widget的析构函数体(该函数体就是放置编译器自动生成销毁std::unique_ptr数据成员的代码的地方)。在widget.h中声明Widget的析构函数但不要定义即析构函数不要在头文件里 inline 默认应放到.cpp中让编译器在有机会看到完整Impl的地方生成它。2. 对于移动赋值运算符对于编译器生成的move赋值运算符它对pImpl再赋值之前需要先销毁它所指向的对象然而在Widget头文件中pImpl指向的仍是一个incomplete type w1 std::move(w2); w1需要先销毁3. 对于移动构造函数它的问题在于编译器通常是在move构造函数内抛出异常的事件中生成析构pImpl的代码而对pImpl析构需要Impl的类型是完整的。问题一样解决方法自然也一样将move操作的定义写在实现文件widget.cpp中如果我们用std::shared_ptr替代std::unique_ptr我们会发现本条款的建议不再适用了。std::unique_ptr和std::shared_ptr的表现行为不同的原因是它们之间支持自定义deleter的方式不同。对于std::unique_ptrdeleter的类型是智能指针的一部分这就使得编译器能够生成更小的运行时数据结构以及更快的运行时代码。更高效率的结果是要求当编译器生成特殊函数时std::unique_ptr所指向的类型必须是完整的。对于std::shared_ptr来说deleter的类型不是智能指针的一部分。虽然会造成大一点的运行时数据结构和稍微慢一些的代码。但是在编译器生成特殊函数时指向的类型不需要是完整的。我们修改代码如下widget.hpp// widget.hpp #pragma once #include memory class Widget { public: Widget(); ~Widget(); Widget(Widget rhs) noexcept; Widget operator(Widget rhs) noexcept; Widget(const Widget rhs) delete; Widget operator(const Widget rhs) delete; void interface(); private: struct Impl; std::unique_ptrImpl pImpl; };widget.cpp#include widget.hpp #include iostream #include memory #include string #include utility struct Widget::Impl { std::string value{UserClassObject}; void concreteInterface() { std::cout value std::endl; } }; Widget::Widget() : pImpl(std::make_uniqueImpl()) { } Widget::~Widget() default; Widget::Widget(Widget rhs) noexcept default; Widget Widget::operator(Widget rhs) noexcept default; void Widget::interface() { pImpl-concreteInterface(); }client.cpp#include widget.hpp #include utility int main() { Widget w1; Widget w2 std::move(w1); Widget w3; w3 std::move(w2); w3.interface(); return 0; }编译通过g -stdc17 -Wall -Wextra -pedantic widget.cpp client.cpp -o widget_demo如果我们在cpp文件中把 ~Widget放到Impl的前面:#include widget.hpp #include iostream #include memory #include string #include utility Widget::~Widget() default; // cpp 中的析构函数放到 Impl定义的前面也是OK的 struct Widget::Impl { std::string value{UserClassObject}; void concreteInterface() { std::cout value std::endl; } }; Widget::Widget() : pImpl(std::make_uniqueImpl()) { } Widget::Widget(Widget rhs) noexcept default; Widget Widget::operator(Widget rhs) noexcept default; void Widget::interface() { pImpl-concreteInterface(); }编译也是通过的虽然写在Impl定义前面但它已经是在.cpp文件里做“类外定义” 了不再是头文件里的 inline 默认析构所以很多编译器/标准库实现下仍然可以通过。核心原因是这两个“时机”不是一回事你写出~Widget() default;的位置编译器真正去实例化std::unique_ptrImpl析构逻辑的时机Widget的析构本质上会去析构成员std::unique_ptrImpl pImpl;而unique_ptrImpl真正触发delete Impl*的检查往往是在它的析构函数模板被实例化的时候。这个实例化很多实现会延后到编译器已经看完整个.cpp那时下面这个定义已经出现了struct Widget::Impl{...};所以它就能通过。为什么头文件里 default常常不行如果你写成头文件里这样class Widget{public:~Widget() default;...};那这个析构函数就是 inline 的任何#include widget.hpp的翻译单元都可能当场去生成它。而这些地方看不到Impl的完整定义于是unique_ptrImpl的析构就会因为Impl是不完整类型而报错。也就是说真正危险的是在.hpp里默认析构而不是在.cpp里类外 default但写在Impl前面但为什么我仍然建议把它放在Impl后面因为这样更清晰也更稳妥struct Widget::Impl{...};Widget::~Widget() default;好处是代码语义更直观先看到完整Impl再定义会销毁它的析构避免读代码的人误解某些实现细节差异下更不容易踩坑面试/代码评审里这是更标准的写法如果我们把move constuctor的default放到hpp文件中// widget.hpp#pragma once #include memory class Widget { public: Widget(); ~Widget(); // Widget(Widget rhs) noexcept; Widget::Widget(Widget rhs) noexcept default; //移动构造default在hpp文件中 Widget operator(Widget rhs) noexcept; Widget(const Widget rhs) delete; Widget operator(const Widget rhs) delete; void interface(); private: struct Impl; std::unique_ptrImpl pImpl; };// widget.cpp#include widget.hpp #include iostream #include memory #include string #include utility // Widget::~Widget() default; struct Widget::Impl { std::string value{UserClassObject}; void concreteInterface() { std::cout value std::endl; } }; Widget::Widget() : pImpl(std::make_uniqueImpl()) { } Widget::~Widget() default; //从cpp文件中删除了 移动构造函数 // Widget::Widget(Widget rhs) noexcept default; Widget Widget::operator(Widget rhs) noexcept default; void Widget::interface() { pImpl-concreteInterface(); }编译错误:In file included from client.cpp:5878:widget.hpp:12:5: error: extra qualification Widget:: on member Widget [-fpermissive]12 |Widget::Widget(Widget rhs) noexcept default;| ^~~~~~In file included from C:/mingw64/lib/gcc/x86_64-w64-mingw32/15.2.0/include/c/memory:80,from widget.hpp:3:C:/mingw64/lib/gcc/x86_64-w64-mingw32/15.2.0/include/c/bits/unique_ptr.h: In instantiation of void std::default_delete_Tp::operator()(_Tp*) const [with _Tp Widget::Impl]:C:/mingw64/lib/gcc/x86_64-w64-mingw32/15.2.0/include/c/bits/unique_ptr.h:399:17: required from std::unique_ptr_Tp, _Dp::~unique_ptr() [with _Tp Widget::Impl;_Dp std::default_deleteWidget::Impl]399 | get_deleter()(std::move(__ptr));| ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~widget.hpp:12:5: required from here12 | Widget::Widget(Widget rhs) noexcept default;| ^~~~~~C:/mingw64/lib/gcc/x86_64-w64-mingw32/15.2.0/include/c/bits/unique_ptr.h:91:23: error: invalid application of sizeof toincomplete type Widget::Impl91 |static_assert(sizeof(_Tp)0,| ^~~~~~~~~~~