
1.2 pimpl惯用法
这里有一个名为CSocketClient的网络通信类,定义如下:


CSocketClient 类的 public 方法提供了对外接口供第三方使用,每个函数的具体实现都在SocketClient.cpp中,对第三方不可见。对于在Windows系统上提供给第三方使用的库,库作者一般需要提供.h、.lib和.dll文件给库使用者,对于Linux系统则需要提供.h、.a或.so文件。
不管在哪种操作系统上,提供像SocketClient.h这样的头文件给第三方使用时,库作者大多会隐隐不安——因为SocketClient.h文件中CSocketClient类的大量成员变量和私有函数都暴露了这个类的太多实现细节,很容易让使用者看出其实现原理。这样的头文件对于一些涉及核心技术实现的库和SDK,是非常敏感的。
那有没有办法既能保持对外接口不变,又能尽量不暴露一些关键的成员变量和私有函数的实现方法呢?有,我们可以将代码稍微修改一下:


在以上代码中,所有的关键成员变量都已经不存在了,取而代之的是一个类型为Impl的指针成员变量m_pImpl。
具体采用什么名称,读者完全可以根据自己的实际情况来定,不一定非要使用“Impl”和“m_pImpl”这样的名称。
Impl 类现在对使用者完全透明,为了在 CSocketClient 类中引用 Impl 类,我们在SocketClient.h文件中使用了一个前置声明(以上加粗代码行),然后就可以将原来属于CSocketClient类的成员变量转移到Impl类中了:

我们接着在CSocketClient构造函数中创建这个m_pImpl对象,在CSocketClient析构函数中释放这个对象:


这样,在 CSocketClient 类内部,对于我们原来直接引用的成员变量,现在可以使用m_pImpl->变量名来引用了。
这里仅以演示隐藏 CSocketClient 的成员变量为例,隐藏类的私有方法与隐藏成员变量的做法相同,即将原来属于CSocketClient的方法变成Impl的方法。
需要强调的是,在实际开发中,由于Impl类是CSocketClient的辅助类,没有独立存在的必要,所以一般会将Impl类定义成CSocketClient的内部类。即采用如下形式:

然后在ClientSocket.cpp中定义Impl类的实现:


现在CSocketClient 这个类除了保留对外的接口,其内部实现用到的变量和方法基本对使用者不可见了。C++中对类的这种封装方法被称为 pimpl 惯用法,即 Pointer to Implementation(也有人认为是Private Implementation)。
在实际开发中,Impl类的声明和定义既可以使用class关键字,也可以使用struct关键字。在C++中,struct类型可以用于定义成员方法,但struct所有的成员变量和方法默认都是public的。
现在总结该方法的优点,如下所述。
◎ 核心数据成员被隐藏,不必暴露在头文件中,对使用者透明,提高了安全性。
◎ 降低了编译依赖,提高了编译速度。原来头文件中的一些私有成员变量可能是非指针、非引用类型的自定义类型,需要在当前类的头文件中包含这些类型的头文件。在使用了 pimpl 惯用法以后,这些私有成员变量就被移动到当前类的 cpp 文件中,因此头文件不再需要包含这些成员变量的类型头文件,当前头文件变得“干净”,其他文件在引用这个头文件时,依赖的类型变少,加快了编译速度。
◎ 接口与实现分离。使用了 pimpl 惯用法之后,即使 CSocketClient 或者 Impl 类的实现细节发生了变化,对使用者都透明,对外的CSocketClient类声明却仍然可以保持不变。例如,我们可以增、删、改 Impl 的成员变量和成员方法,而保持SocketClient.h文件的内容不变;如果不使用pimpl惯用法,则我们做不到不改变SocketClient.h文件而增、删、改CSocketClient类的成员。
C++11标准引入了智能指针对象,我们可以使用std::unique_ptr对象来管理上述用于隐藏具体实现的m_pImpl指针。可以将SocketClient.h文件修改如下:


在SocketClient.cpp中修改CSocketClient对象的构造函数和析构函数,如果编译器仅支持C++11标准,则可以这么修改:

如果编译器支持C++14及以上标准,则可以这么修改:

由于已经使用了智能指针来管理 m_pImpl 指向的堆内存,所以在析构函数中不再需要显式地释放堆内存:

pimp惯用法是C/C++项目开发中一种非常实用的代码编写策略,建议读者掌握它。