智能指针——当使用 Pimpl 惯用法时,在实现文件中定义特殊成员函数

智能指针——当使用 Pimpl 惯用法时,在实现文件中定义特殊成员函数 文章目录当使用 Pimpl 惯用法时在实现文件中定义特殊成员函数Pimpl 惯用法C11 智能指针版本的 Pimpl解决方案在实现文件中定义特殊成员函数完整 Pimpl 示例unique_ptr vs shared_ptr 在 Pimpl 中的区别当使用 Pimpl 惯用法时在实现文件中定义特殊成员函数核心要点要点1Pimpl 惯用法通过减少类实现和类使用者之间的编译依赖来减少编译时间2对于unique_ptr类型的 pImpl 指针需在头文件中声明特殊成员函数在实现文件中定义它们。即使编译器默认版本可用也必须这样做3以上建议**只适用于 **unique_ptr不适用于shared_ptr延伸不止是 Pimpl只要类中有指向不完整类型的 unique_ptr 成员就需要将特殊成员函数的定义延迟到实现文件中。Pimpl 惯用法PimplPointer toImplementation指涉到实现的指针也称编译防火墙Compilation Firewall。核心思想将类的实现细节私有成员隐藏到一个前向声明的实现类中公共接口只持有一个指向该实现的指针。简单说就是把怎么实现的从 .h 挪到 .cpp头文件只暴露干净的接口代价是多一次间接访问和一次堆分配。// widget.hclassWidget{public:Widget();// ...private:std::string name;std::vectordoubledata;Gadget g1,g2,g3;// Gadget 是用户自定义类型};:::color4⚠️**问题**Widget 的使用者必须 #include 、 和 gadget.h。结果是编译时间增加依赖传递——头文件内容一旦变化所有使用者都必须重新编译:::// widget.h classWidget{public:Widget();~Widget();// ...private:structImpl;// 只声明不定义不完整类型Impl*pImpl;// 指向实现的原始指针};// widget.cpp #includewidget.h#includegadget.h#includestring#includevectorstructWidget::Impl{// 在 .cpp 中完整定义std::string name;std::vectordoubledata;Gadget g1,g2,g3;};Widget::Widget():pImpl(newImpl){}Widget::~Widget(){deletepImpl;}C11 智能指针版本的 Pimpl// widget.h classWidget{public:Widget();// 注意没有声明析构函数// ...private:structImpl;std::unique_ptrImplpImpl;// 用智能指针替代原始指针};// widget.cpp #includewidget.h#includegadget.h#includestring#includevectorstructWidget::Impl{std::string name;std::vectordoubledata;Gadget g1,g2,g3;};Widget::Widget():pImpl(std::make_uniqueImpl())// 根据 Item 21优先用 make_unique{}#includewidget.hWidget w;// 编译错误⚠️为什么编译失败—— 根本原因分析错误链路用户没有声明析构函数 → 编译器自动生成析构函数且为inline自动生成的析构函数调用pImplunique_ptrWidget::Impl的析构函数unique_ptr的默认删除器在delete之前使用 C11 的static_assert检查指针指向的类型是否是完整类型在头文件中Widget::Impl只有声明 → 是不完整类型static_assert(sizeof(_Tp) 0, ...)失败 → 编译报错核心矛盾编译器自动生成的特殊成员函数是inline的因此它在头文件中被实例化而此时Widget::Impl的定义在.cpp中还不可见。解决方案在实现文件中定义特殊成员函数// widget.h classWidget{public:Widget();~Widget();// ✅ 只有声明// ...private:structImpl;std::unique_ptrImplpImpl;};// widget.cpp #includewidget.h#includegadget.h// ...structWidget::Impl{/* ... 完整定义 ... */};Widget::Widget():pImpl(std::make_uniqueImpl()){}Widget::~Widget()default;// ✅ 在 Impl 定义之后定义// widget.h classWidget{public:Widget();~Widget();Widget(Widgetrhs);// ✅ 只声明Widgetoperator(Widgetrhs);// ✅ 只声明private:structImpl;std::unique_ptrImplpImpl;};// widget.cpp // ... struct Widget::Impl { ... };Widget::~Widget()default;Widget::Widget(Widgetrhs)default;// ✅ 在实现文件中定义WidgetWidget::operator(Widgetrhs)default;// ✅ 在实现文件中定义为什么移动操作也要在实现文件中定义移动赋值操作符在重新赋值之前需要先销毁pImpl移动构造函数在抛出异常时需要销毁pImpl——两者都需要Impl是完整类型。// widget.h classWidget{public:Widget();~Widget();Widget(Widgetrhs);Widgetoperator(Widgetrhs);Widget(constWidgetrhs);// ✅ 只声明Widgetoperator(constWidgetrhs);// ✅ 只声明private:structImpl;std::unique_ptrImplpImpl;};// widget.cpp // ...Widget::Widget(constWidgetrhs)// 深拷贝:pImpl(std::make_uniqueImpl(*rhs.pImpl)){}WidgetWidget::operator(constWidgetrhs)// 深拷贝赋值{*pImpl*rhs.pImpl;return*this;}完整 Pimpl 示例#includememory// std::unique_ptr — 用于管理 Impl 的独占所有权classWidget{public:// -----------------------------------------------------------------------// 构造与析构// -----------------------------------------------------------------------/// 默认构造函数/// 注意必须在 .cpp 文件中实现不能在头文件中内联/// 因为在头文件中 Impl 是不完整类型unique_ptr 的析构需要完整类型才能 delete。Widget();/// 析构函数/// 同样必须在 .cpp 中实现原因同上。/// 即使在头文件中写 default编译器也会因为 Impl 不完整而报错。~Widget();// -----------------------------------------------------------------------// 移动语义// -----------------------------------------------------------------------/// 移动构造函数/// 将 rhs 的 pImpl 所有权转移给 *thisrhs.pImpl 变为 nullptr。/// 使用 std::unique_ptr 的移动语义自动完成通常实现为/// Widget::Widget(Widget rhs) default;/// 由于 unique_ptr 不可拷贝编译器生成的移动操作是正确且高效的。Widget(Widgetrhs);/// 移动赋值运算符/// 先释放自身 pImpl再接管 rhs 的资源。/// 必须在 .cpp 中实现因为需要先销毁旧的 Impl要求完整类型。Widgetoperator(Widgetrhs);// -----------------------------------------------------------------------// 拷贝语义// -----------------------------------------------------------------------/// 拷贝构造函数/// 深拷贝 rhs.pImpl 指向的 Impl 对象。/// 必须在 .cpp 中实现因为需要知道 Impl 的完整定义才能拷贝它。Widget(constWidgetrhs);/// 拷贝赋值运算符/// 先释放旧的 pImpl若自身已有再深拷贝 rhs 的数据。/// 典型实现会使用 copy-and-swap 惯用法保证强异常安全。Widgetoperator(constWidgetrhs);private:// 前置声明仅声明而不定义外部对此一无所知// 在 .cpp 文件中才给出完整定义包含所有私有数据成员structImpl;// 指向实现的独占指针// - 使用 unique_ptr 保证独占所有权Widget 销毁时自动释放 Impl// - 指针大小固定通常 8 字节因此 Widget 的 sizeof 不受 Impl 变化影响// - 使用 unique_ptr 而非 raw pointer自动管理生命周期防止内存泄漏std::unique_ptrImplpImpl;};// // widget.cpp — Widget 类的 Pimpl 实现// //// 本文件包含了 Widget 类的所有私有实现细节。// 修改本文件中的任何内容如 Impl 的成员、构造逻辑等// 只会触发 widget.cpp 重新编译不会波及包含 widget.h 的其他翻译单元。// 这就是 Pimpl 惯用法的核心价值所在。#includewidget.h// 仅包含公开接口头文件#includegadget.h// Gearget 类型 — 仅 .cpp 需要widget.h 的使用者无需包含#includestring// std::string#includevector// std::vector// // Widget::Impl — 私有实现结构体的完整定义// //// 在 widget.h 中此处仅为前置声明 struct Impl;外部对此一无所知。// 在此才给出完整定义所有私有数据成员都放在这里// - 数据成员的类型、数量发生变化时Widget 的 sizeof 保持不变// - 新增成员不会导致 widget.h 的使用者重新编译//structWidget::Impl{std::string name;// 名称std::vectordoubledata;// 数值数据集合Gadget g1,g2,g3;// 三个 Gearget 组件};// // 构造函数 — 分配 Impl 对象// /// 默认构造函数/// 使用 std::make_unique 在堆上构造 Impl 实例并将所有权交给 pImpl。/// make_unique 是 C14 引入的便捷函数等价于/// pImpl(std::unique_ptrImpl(new Impl()))/// 但更安全不会因中间异常导致裸指针泄漏。Widget::Widget():pImpl(std::make_uniqueImpl())// 成员初始化列表构造 Impl 并转移所有权{}// // 析构函数 — 释放 Impl 对象// /// 析构函数/// 使用 default 让编译器自动生成。/// 关键在于此处必须放在 Impl 完整定义之后。/// 如果在头文件中写 ~Widget() default编译器会在头文件处尝试析构/// unique_ptrImpl而此时 Impl 是不完整类型编译器无法生成 delete 代码/// 会导致编译错误或未定义行为取决于编译器。////// default 生成的析构函数会/// 1. 析构 pImplunique_ptr进而调用 delete 释放 Impl/// 2. 无需手动释放 g1, g2, g3 — 它们作为 Impl 的成员会被自动析构Widget::~Widget()default;// // 移动语义 — 廉价的所有权转移// /// 移动构造函数/// default 即可正确工作编译器生成的移动构造函数会将 rhs.pImpl 的所有权/// 移交给 *this随后 rhs.pImpl 变为 nullptr。/// 这是 O(1) 操作仅交换一个指针通常 8 字节不涉及任何堆分配或深拷贝。Widget::Widget(Widgetrhs)default;/// 移动赋值运算符/// default 的工作流程/// 1. 先销毁 *this 的 pImpl释放旧的 Impl 内存/// 2. 再将 rhs.pImpl 的所有权转移到 *this/// 3. rhs.pImpl 变为 nullptr/// 同样是 O(1) 操作。WidgetWidget::operator(Widgetrhs)default;// // 拷贝语义 — 昂贵的深拷贝// /// 拷贝构造函数/// 深拷贝 rhs.pImpl 指向的 Impl 对象。/// 使用 std::make_uniqueImpl(*rhs.pImpl) 在堆上构造一个新的 Impl/// 其内容通过 Impl 的拷贝构造函数从 *rhs.pImpl 逐成员复制。////// 复制的内容包括/// - namestd::string 深拷贝可能涉及堆分配/// - datastd::vectordouble 深拷贝所有元素/// - g1, g2, g3各自调用 Gearget 的拷贝构造函数////// 这是 O(n) 操作开销取决于各成员的拷贝代价。Widget::Widget(constWidgetrhs):pImpl(std::make_uniqueImpl(*rhs.pImpl))// 解引用 unique_ptr 得到 Impl拷贝构造之{}/// 拷贝赋值运算符/// 当前实现使用逐成员拷贝赋值*pImpl *rhs.pImpl而非 copy-and-swap。////// 工作流程/// 1. 调用 Impl 的拷贝赋值运算符编译器生成的逐成员赋值/// 2. name 被 rhs.name 覆盖std::string 的赋值可能复用已有缓冲区/// 3. data 被 rhs.data 覆盖std::vector 的赋值可能涉及重新分配/// 4. g1, g2, g3 各自被覆盖Gearget 的赋值运算符////// 注意此实现假设 rhs.pImpl 非空因为 rhs 是 const 引用应为合法对象。////// 对比 copy-and-swap 写法/// Widget temp(rhs); // 先拷贝构造一个临时对象/// std::swap(pImpl, temp.pImpl); // 交换指针temp 析构时释放旧资源/// return *this;/// copy-and-swap 提供强异常安全保证要么全成功要么全不变/// 当前写法只提供基本异常安全部分成员可能已修改后抛出异常。WidgetWidget::operator(constWidgetrhs){*pImpl*rhs.pImpl;// 委托给 Impl 的拷贝赋值运算符return*this;// 返回 *this支持链式赋值 a b c}unique_ptr vs shared_ptr 在 Pimpl 中的区别// widget.h classWidget{public:Widget();// 没有析构函数和移动操作的声明// ...private:structImpl;std::shared_ptrImplpImpl;// 换成 shared_ptr};// 客户代码可以直接使用Widget w1;autow2(std::move(w1));// 移动构造 ✅w1std::move(w2);// 移动赋值 ✅什么行为不同特性std::unique_ptrstd::shared_ptr删除器是类型的一部分✅ 是更小更快❌ 不是稍大稍慢编译器生成特殊成员函数时要求指向的类型是完整类型不要求指向的类型是完整类型 对于 PimplWidget 对 Impl 拥有独占所有权因此 unique_ptr是语义上更合适的选择。多付出的代价手动在实现文件中定义特殊成员函数是值得的。