Pimpl(Pointer to Implementation,指向实现的指针)是一种经典的 C++ 设计模式,它的核心思想是:
把类的具体实现隐藏起来,只在头文件中保留一个指向实现类的指针。
这样可以实现信息隐藏、降低编译依赖、提高编译速度、保持 ABI 稳定。
为什么需要 Pimpl?
假设没有使用 Pimpl。
Robot.h
cpp
class Robot
{
public:
void connect();
void move();
private:
TcpClient tcp;
JsonParser parser;
Logger logger;
ThreadPool pool;
};
这里 Robot.h 包含了很多头文件:
cpp
#include "TcpClient.h"
#include "JsonParser.h"
#include "Logger.h"
#include "ThreadPool.h"
于是:
Robot.h
│
├── TcpClient.h
├── Logger.h
├── JsonParser.h
└── ThreadPool.h
如果有 100 个 .cpp 文件都包含了 Robot.h:
main.cpp
ui.cpp
server.cpp
test.cpp
...
↓
#include "Robot.h"
现在只要修改了:
Logger.h
哪怕只是增加一个成员变量:
class Logger
{
...
int level;
};
那么:
Logger.h 改变
↓
Robot.h 改变
↓
所有 include Robot.h 的 cpp
全部重新编译
大型项目可能重新编译几分钟甚至几十分钟。
Pimpl 怎么解决?
把真正的数据全部放到 .cpp
Robot.h
cpp
class RobotImpl;
class Robot
{
public:
Robot();
~Robot();
void connect();
void move();
private:
RobotImpl* impl;
};
头文件现在只有:
Robot
↓
RobotImpl*
根本不知道里面是什么。
Robot.cpp
cpp
#include "Robot.h"
#include "TcpClient.h"
#include "Logger.h"
#include "ThreadPool.h"
class RobotImpl
{
public:
TcpClient tcp;
Logger logger;
ThreadPool pool;
};
然后:
cpp
Robot::Robot()
{
impl = new RobotImpl();
}
调用:
cpp
void Robot::connect()
{
impl->tcp.connect();
}
编译依赖发生了什么变化?
以前:
Robot.h
↓
TcpClient.h
Logger.h
JsonParser.h
...
现在:
Robot.h
↓
什么都不知道
↓
只有 Robot.cpp
↓
知道 TcpClient
所以:
Logger.h 修改
↓
Robot.cpp 重新编译
↓
main.cpp 不用重新编译
编译速度提升很多。
更现代的写法
现代 C++ 不建议手写 new/delete。
一般使用:
cpp
class RobotImpl;
class Robot
{
public:
Robot();
~Robot();
private:
std::unique_ptr<RobotImpl> impl;
};
构造:
cpp
Robot::Robot()
: impl(std::make_unique<RobotImpl>())
{
}
析构:
cpp
Robot::~Robot() = default;
这是目前最常见的 Pimpl 写法。
Pimpl 的工作流程
假设用户调用:
cpp
Robot robot;
robot.connect();
流程:
用户
↓
Robot
↓
impl
↓
RobotImpl
↓
TcpClient
↓
TCP连接
真正干活的是:
RobotImpl
Robot 只是一个对外接口。
Pimpl 的优点
1. 隐藏实现细节
用户只能看到:
cpp
Robot robot;
看不到:
TcpClient
JsonParser
ThreadPool
有利于封装。
2. 降低编译依赖
这是 Pimpl 最大的价值。
例如:
ThreadPool 修改
↓
只编译 Robot.cpp
↓
不用编译整个项目
3. 保持 ABI 稳定
假设 SDK 发布:
cpp
class Robot
{
private:
RobotImpl* impl;
};
以后:
RobotImpl 增加:
cpp
Cache cache;
甚至:
TcpClient
↓
MQTTClient
Robot 类的大小仍然只是一个指针,因此很多情况下不会破坏二进制接口(ABI),用户无需重新编译即可替换新的动态库(具体还取决于平台、编译器和接口变更)。
4. 减少头文件包含
Robot.h
不用:
cpp
#include "Logger.h"
#include "TcpClient.h"
#include "JsonParser.h"
只需要:
cpp
#include <memory>
Pimpl 的缺点
多了一次间接访问
cpp
impl->connect();
相比直接访问成员,会多一次指针解引用。不过对于大多数业务程序,这点性能开销通常可以忽略。
实现稍复杂
需要定义 Impl 类,并正确管理其生命周期
调试稍麻烦
调试时需要进入 RobotImpl 才能看到真正的数据成员