C++ Pimpl设计模式详解

在阅读一些大型 C++ 项目源码时,经常会看到类似下面这样的代码:
|
|
第一次看到这种写法,很多人都会疑惑:
- 为什么真正的数据成员不放在类里面?
- 为什么要多定义一个
Impl类? - 为什么析构函数要放到
.cpp中实现? - 这样设计有什么好处?
实际上,这就是C++中非常经典的Pimpl(Pointer to Implementation)设计模式。本文将从设计思想、实现方式、优缺点以及使用场景几个方面详细介绍Pimpl。
什么是Pimpl?
Pimpl是Pointer to Implementation的缩写,即指向实现的指针。它的核心思想非常简单:
将类的实现细节隐藏到另一个实现类(Impl)中,对外只暴露一个指向实现对象的指针。
整体结构如下:

MyClass负责提供接口,Impl负责真正的实现。这样,用户只能看到接口,而无法看到内部实现细节。
为什么需要Pimpl?
先来看一个普通的类。
|
|
这种写法看起来没有问题,但实际上存在几个缺点。
1. 暴露实现细节
任何包含MyClass.h的代码,都能知道:
- 使用了
std::vector - 元素类型是
std::string
如果以后希望改成std::list<std::string>或者std::unordered_map<int, std::string>,头文件就必须修改。
2. 编译依赖严重
假设项目结构如下:

如果修改了std::vector<std::string>,即使只是改成std::vector<int>,所有包含该头文件的源文件都会重新编译。大型项目中,一个头文件可能被几千个源文件包含。因此,一次小修改,可能导致整个项目重新编译。
3. ABI不稳定
ABI(Aplication Binary Interface,应用二进制接口)可以理解为程序编译完成后的二进制层面的约定。对于动态库来说:
|
|
后来增加一个成员:
|
|
对象大小会发生变化。已经发布的程序可能会因为对象布局变化而出现ABI不兼容的问题。
Pimpl如何解决这些问题?
采用Pimpl后:
|
|
头文件只有一个前向声明和一个智能执政。完全没有暴露任何实现细节,真正的实现会全部放在.cpp中。
|
|
以后即使把std::vector<std::string>改成std::unordered_map<int, std::string>也无需修改头文件。只有MyClass.cpp需要重新编译。
为什么析构函数不能写在头文件?
很多人第一次写Pimpl都会这样:
|
|
编译器会报错:
|
|
原因在于std::unique_ptr<T>默认用delete释放对象。而执行delete impl_必须知道class Impl的完整定义。但是头文件只有class Impl;这个前向声明,属于不完整类型(Incomplete type)。因此,析构函数需要放到.cpp中实现。
|
|
|
|
此时Impl已经完整定义,unique_ptr就能正常析构。
为什么推荐使用std::unique_ptr?
早期C++常见写法:
|
|
构造:
|
|
析构:
|
|
现代C++推荐std::unique_ptr<Impl> impl_,优点包括:自动释放资源(RAII)、异常安全等。由于std::unique_ptr不能复制,因此:
|
|
默认无法编译。通常有三种处理方式:
方式一:禁止复制
|
|
方式二:支持移动
|
|
方式三:实现深拷贝
如果希望支持复制,可以自己实现:
|
|
前提是Impl本身支持复制。
ABI为什么更加稳定?
对于普通类:
|
|
对象大小会随着成员变化而变化。而Pimpl:
|
|
无论增加成员、删除成员、修改成员类型,变化都发生Impl内部。MyClass始终只有一个指针成员,因此对象布局保持不变。这也是一些大型框架使用Pimpl的重要原因之一。
Pimpl的优缺点
优点:
- 隐藏实现细节
- 降低头文件依赖
- 减少大规模重新编译
- 提高 ABI 稳定性
- 更利于库的版本升级
缺点:
- 多了一次动态内存分配
- 每次访问成员需要一次指针间接访问
- 实现代码稍复杂
- 调试时需要跳转到
Impl - 对于简单的小型类,收益并不明显
什么时候适合使用Pimpl?
Pimpl并不是所有类都需要使用,它更适用于以下场景:
- 对外发布的 SDK 或公共库
- 需要保持 ABI 稳定的动态库
- 大型工程,减少头文件依赖和编译时间
- 类依赖大量 STL 或第三方库
- 跨平台开发,需要隐藏平台相关实现
如果只是一个普通的数据结构,例如:
|
|
或者一个简单的业务对象,就没有必要使用Pimpl。
总结
Pimpl的本质可以概括为一句话:
把实现放到
.cpp,把接口留在.h。
它最大价值体现在三个方面:
- 隐藏实现细节,降低模块耦合。
- 减少头文件依赖,显著缩短大型项目的编译时间。
- 保持 ABI 稳定,方便动态库长期维护和升级
对于大型 C++ 项目而言,Pimpl 已经成为一种经典且成熟的设计模式。但它并非银弹,对于简单类或性能极其敏感的场景,应权衡额外的堆分配和间接访问成本,再决定是否采用。
推荐
相关内容
支付宝
微信