设计模式 PImpl (指向实现的指针),也称为 不透明指针,作为一种在 C++ 中分离类的接口和实现的手段而出现。这对于确保二进制接口 (ABI) 兼容性、加速编译和隐藏类实现细节非常重要。
问题背景。
在许多 C++ 项目中,必须在不改变公共接口和不重新编译这些类的客户端的情况下修改类的实现。问题在于,任何对头文件的更改都需要重新编译所有依赖模块,这在大型代码库中可能极其高昂。PImpl 允许最小化重新编译并提供更好的封装。
问题。
在头文件中使用私有成员定义类的标准方法要求在编译时了解所有这些成员。当扩展或修改它们时,必须重新编译包括该头文件的所有文件。此外,这会暴露客户端的实现/结构细节,可能对安全性和架构完整性产生负面影响。
解决方案。
PImpl 通过使用指向在 cpp 中定义的前向声明结构实现 (Impl struct/class) 的指针来隐藏实现。这允许在不影响接口的情况下更改实现。
代码示例:
// Widget.h class Widget { public: Widget(); ~Widget(); void doSomething(); private: struct Impl; Impl* pimpl; // 不透明指针 }; // Widget.cpp #include "Widget.h" struct Widget::Impl { int secret; }; Widget::Widget() : pimpl(new Impl{42}) {} // 内部秘密 Widget::~Widget() { delete pimpl; } void Widget::doSomething() { pimpl->secret += 1; }
关键特点:
可以在 PImpl 中使用 std::unique_ptr 代替原始指针吗?
是的,现代且安全的方法是使用 std::unique_ptr(或 std::shared_ptr,如果需要共享所有权)。这可以正确管理内存,而不必为原始指针显式编写析构函数/复制运算符:
private: std::unique_ptr<Impl> pimpl;
可以使具有 PImpl 的类可移动但不可复制吗?
可以,如果提供移动构造函数/运算符,但删除复制构造函数。例如:
Widget(Widget&&) noexcept = default; Widget& operator=(Widget&&) noexcept = default; Widget(const Widget&) = delete; Widget& operator=(const Widget&) = delete;
使用 PImpl 会对性能产生开销吗?
是的,由于解引用指针和额外的动态内存分配(堆分配)。对于性能关键的结构,这可能是一个显著的缺点。
一家大型公司对所有类都实施了 PImpl,包括简单的数据结构,这导致简单操作由于频繁解引用指针而显著减慢。
优点:
缺点:
在一个长期存在的用户界面库项目中,仅对具有频繁变化内部结构的复杂小部件应用了 PImpl,从而为第三方客户端保持稳定的 ABI。
优点:
缺点: