- Pimpl (pointer-to-implementation) 是一种常用的类设计手法,可以实现:(1)只暴露函数调用接口,隐藏实现细节;(2)避免修改实现时导致调用者的代码必须重新编译,提高构建速度。实现 Pimpl 的方法正如其名:头文件(.h)中,去除类中所有私有成员;声明(而不定义)一个 impl 类型,为类添加一个 private 的 impl 指针。实现文件中,定义 impl 类型,包含所有私有成员,其它函数实现通过 impl 指针访问它们;构造和析构函数实现中,new 和 delete impl 指针。如下所示:
widget.h:
class Widget
{
public:
Widget();
~Widget();
// 接口函数...
private:
struct Impl; // 只声明不定义
Impl *pImpl;
};
widget.cpp:
#include "widget.h"
#include <string>
#include <vector>
struct Widget::Impl {
std::string name;
std::vector<double> data;
};
Widget::Widget() : pImpl(new Impl)
{}
Widget::~Widget()
{ delete pImpl; }
在 widget.h 中,只声明不定义的 Impl 是一种不完整类型(imcomplete type)。对于不完整类型能做的操作很有限,而定义其指针是可以的(例如另一个广泛的应用:前向声明)。
- 在C++11中,我们自然会想到用智能指针来代替
Impl *
,本场景下最适合的自然是unique_ptr
。由于智能指针在生命周期结束时自动析构其管理的对象,现在我们不需要在析构函数中做任何事情了。遗憾的是,如果如此修改指针并删除析构函数,Widget类在调用处会出现编译错误:
- 错误信息告诉我们
unique_ptr
在析构尝试 delete 掉内部的 Impl 指针时遇到了不完整类型。出现该问题的原因是,因为我们没有声明一个 Widget 的析构函数,编译器为我们自动生成了一个(Item 17),并在其中调用成员unique_ptr<Impl> pImpl
的析构函数,unique_ptr
的默认 delete 会先使用C++11的static_assert
检查内部指针是否是不完整类型,从而失败。编译器自动生成的析构函数是正确的,实际上我们又知道 Impl 的定义是在实现文件(cpp)中给出的,所以为了解决该问题,我们需要做的是告诉编译器将自动生成延迟到实现文件中给出,代码如下:
widget.h:
#include <memory>
class Widget
{
public:
Widget();
~Widget(); // 仅声明
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
widget.cpp:
#include "widget.h"
#include <string>
#include <vector>
struct Widget::Impl {
std::string name;
std::vector<double> data;
};
Widget::Widget() : pImpl(std::make_unique<Impl>())
{}
Widget::~Widget() = default; // 析构函数定义,使用编译器自动生成版本
-
显式声明析构函数后,编译器不会再自动生成移动构造/赋值函数,尽管默认生成版本的行为通常也是正确的(对内部的
unique_ptr
进行移动操作)。如果需要支持移动,则实现方式与以上析构函数的做法完全相同:头文件中仅声明,实现文件中定义=default
。 -
对于拷贝构造/赋值,由于
unique_ptr
不支持拷贝(就算支持也只是对指针的浅拷贝,而我们想要的是对整个类数据的深拷贝),需要自己给出实现:
widget.h:
class Widget {
public:
...
Widget(const Widget& rhs); // 仍然是仅声明
Widget& operator=(const Widget& rhs);
...
};
widget.cpp:
Widget::Widget(const Widget& rhs) // copy ctor
: pImpl(std::make_unique<Impl>(*rhs.pImpl)) // 使用 make_unique
{}
Widget& Widget::operator=(const Widget& rhs) // copy operator=
{
*pImpl = *rhs.pImpl; // 借用了Impl类自动生成的逐元素的复制
return *this;
}
- 值得注意的是,以上讨论针对的都是
unique_ptr
;如果使用shared_ptr
,则以上建议都不成立。原因是unique_ptr
和shared_ptr
内部实现上的区别:对于unique_ptr
,deleter 是其类型的一部分,因此能让编译器生成更小更快的运行时代码,也意味着使用编译器自动生成的特殊函数时指向的类型必须是完整类型;而对于shared_ptr
,deleter 不是其类型一部分,运行时效率会相对差一些,但不要求是指向类型是完整类型。 - 对于 Pimpl 设计,无疑还是应该使用最合适的
unique_ptr
实现。不过以上信息告诉我们在一些其它适合使用shared_ptr
的场景,无需考虑unique_ptr
的这些函数定义上的小障碍。
总结
- Pimpl 手法通过减少类调用和类实现间的编译依赖提升构建效率。
- 对于
std::unique_ptr pImpl
指针,在头文件中声明特殊成员函数,但在实现文件中实现它们。即使可以使用默认实现也要这样做。 - 以上建议适用于
std::unique_ptr
而不适用于std::shared_ptr
。