Pimpl是“pointer to implementation”的缩写, 该技巧可以避免在头文件中暴露私有细节。Pimpl利用了c++的一个特点,即可以将类的数据成员定义为指向某个已经声明过的类型的指针, 这里的类型仅仅作为名字引入, 并没有被完整地定义。
例如“自动定时器”的API, 它是一个具名对象,当对象被销毁时打印出其生存时间。
- AutoTimer.h 如下:
#ifdef _WIN32
#include <windows.h>
#else
#include <sys/time.h>
#endif
#include <string>
class AutoTimer
{
public:
explicit AutoTimer(const std::string name);
~AutoTimer();
private:
double getElapsed() const;
std::string mName;
#ifdef _WIN32
DWORD mStartTime;
#else
struct timeval mStartTime;
#endif
};
从上面的h文件可知,该API暴露了定时器在不同平台上存储的底层细节,利用Pimpl特性, 可轻易的隐藏掉这些底层实现的细节, 利用Pimpl特效后新的h文件如下:
#include <string>
class AutoTimer{
public:
explicit AutoTimer(const std::string& name);
~AutoTimer();
private:
class Impl;
Impl* mImpl;
};
新API更简洁, 没有任何平台相关的预处理指令, 他人也不能通过头文件了解类的任何私有成员。
现在AutoTimer的功能实现都放在内嵌类Impl中了, 具体的AutoTimer的cpp文件如下:
#include "AutoTimer.h"
#ifdef _WIN32
#include <windows.h>
#else
#include <sys/time.h>
#include <iostream>
#endif
class AutoTimer::Impl
{
public:
double getElapsed() const
{
#ifdef _WIN32
return (GetTickCount() - mStartTime) / 1e3;
#else
struct timeval end_time;
gettimeofday(&end_time, NULL);
double t1 = mStartTime.tv_usec / 1e6 + mStartTime.tv_sec;
double t2 = end_time.tv_usec / 1e6 + end_time.tv_sec;
return t2 - t1;
#endif
}
std::string mName;
#ifdef _WIN32
DWORD mStartTime;
#else
struct timeval mStartTime;
#endif
};
AutoTimer::AutoTimer(const std::string &name)
: mImpl(new AutoTimer::Impl())
{
mImpl->mName = name;
#ifdef _WIN32
mImpl->mStartTime = GetTickCount();
#else
gettimeofday(&mImpl->mStartTime, NULL);
#endif
}
AutoTimer::~AutoTimer()
{
std::cout << mImpl->mName << ":took " << mImpl->getElapsed() << " secs" << std::endl;
delete mImpl;
mImpl = NULL;
}
将Impl类声明为AutoTimer类的私有内嵌类, 可以避免与该实现相关的符号污染全局命名空间。
- Pimpl的优点:
- 信息隐藏, 将具体实现放在cpp文件中, h文件只暴露公有接口;
- 降低耦合;
- 加速编译;
- 更好的二进制兼容性,二进制兼容性指的是在升级库文件的时候(windows下的dll, uinx下的so),不必重新编译使用这个库的可执行文件或使用这个库的其他库文件,程序的功能不被破坏;
- 惰性分配;
- 缺点
- 添加了一层封装, 可能进入性能问题;
- 代码可读性降低;