Pimpl惯用法核心是头文件仅声明不透明指针和接口,实现细节全移至.cpp中;需显式声明析构/拷贝/移动函数并在.cpp定义,因unique_ptr需Impl完整定义才能生成正确代码。
Pimpl 惯用法不是“加个指针就完事”,核心在于:它只在头文件里暴露接口声明和一个不透明指针(std::unique_ptr 或裸指针),所有实现细节、私有成员、第三方头文件依赖,全部挪进 .cpp 文件里 —— 这才是编译防火墙生效的前提。
class Widget { private: struct Impl; st
d::unique_ptr pImpl; }; 不能直接编译因为 std::unique_ptr 在构造/析构/拷贝时需要知道 Impl 的完整定义(否则无法调用其析构函数)。但头文件里只前向声明了 struct Impl;,没定义它。
error: invalid use of incomplete type 'struct Widget::Impl'(尤其在类析构函数隐式生成时爆发)Widget 的析构函数声明,并在 .cpp 中定义(哪怕只写 {})copy constructor、copy assignment operator、move constructor 等也需显式声明并在 .cpp 中定义(或 = default,但前提是 Impl 已定义)#include ,且 pImpl 初始化必须在构造函数初始化列表中完成(如 : pImpl{std::make_unique()} ).h 和 .cpp 文件头文件只保留契约;实现文件才“知情”。任何对 Impl 成员的访问(包括构造、析构、函数调用),都必须发生在 .cpp 中。
widget.h:只含前向声明 + 公共接口 + std::unique_ptr 成员 + 显式析构/赋值声明widget.cpp:先 #include "widget.h",再 struct Widget::Impl { ... }; 完整定义,最后实现所有成员函数(包括委托给 pImpl->xxx() 的逻辑)、)只能出现在 .cpp,绝不能泄露到 .h
Impl 需要访问 Widget 的私有成员,可将 Impl 声明为 friend class Impl;(但慎用,优先考虑接口委托)std::unique_ptr 还是裸指针 + new/delete?现代 C++ 应无条件选 std::unique_ptr,但要注意它带来的约束:
std::unique_ptr 默认禁止拷贝(符合 Pimpl 设计本意),移动是安全的pImpl,而应实现深拷贝逻辑(在 .cpp 中 new 一个新的 Impl 并复制内容) 依赖,但必须手动管理生命周期,极易泄漏或 double-delete;且无法自动支持移动语义std::unique_ptr 是零开销抽象,pImpl.get() 和裸指针访问成本相同/* widget.h */ #pragma once #includeclass Widget { public: Widget(); ~Widget(); // 必须声明 Widget(const Widget&); // 若需拷贝,必须声明并定义在 .cpp 中 Widget& operator=(const Widget&); Widget(Widget&&) noexcept; Widget& operator=(Widget&&) noexcept;
void doSomething(); int getValue() const;private: struct Impl; std::unique_ptr
pImpl; }; /* widget.cpp */ #include "widget.h" #include#include // 所有实现依赖(如 #include )都在这里,绝不进 .h struct Widget::Impl { std::string data; std::vector
cache; // 可以自由包含任意重型头文件、定义复杂类型、链接第三方库 }; Widget::Widget() : pImpl{std::make_unique
()} {} Widget::~Widget() = default; // 此处 Impl 已定义,可 default Widget::Widget(const Widget& other) : pImpl{std::make_unique
(*other.pImpl)} {} Widget& Widget::operator=(const Widget& other) { if (this != &other) pImpl = other.pImpl; return *this; }
void Widget::doSomething() { pImpl->data += "done"; } int Widget::getValue() const { return static_cast
(pImpl->data.size()); } 最容易被忽略的一点:Pimpl 的代价是间接访问(一次指针解引用)、堆分配(除非用 placement new + 内存池优化)、以及强制用户写更多样板代码(析构/移动/拷贝的显式控制)。它解决的是编译依赖爆炸问题,不是运行时性能问题 —— 别为了“看起来更封装”而滥用。