考虑 Widget
类实现如下:
class Widget {
public:
Widget();
...
private:
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};
Widget
的头文件中必须添加 <string>
<vector>
gadget.h
,它们会增加 Widget
的客户的编译时间,且如果 gadget.h
发生改动,依赖它的 Widget
也必须重新编译。
使用C++98的Pimpl习惯用法,可以用一个指向已声明但未定义的结构体的裸指针来替换 Widget
的数据成员:
class Widget {
public:
Widget();
...
private:
struct Impl;
Impl* pImpl;
};
然后,动态分配和回收持有原有的哪些数据成员对象。分配和回收代码则放在实现文件中。
C++11中,使用 std::unique_ptr
替代 Impl
裸指针,头文件代码变成这样:
class Widget {
public:
Widget();
...
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
实现文件是这样:
#include "widget.h"
#include "gadget.h"
#include <string>
#include <vector>
struct Widget::Impl {
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};
Widget::WIdget()
: Pimp(std::make_unique<Impl>()) {}
然而,连最简单的客户代码都无法通过编译:
#include "widget.h"
Widget w; // error!
问题是由 w
被析构(如离开作用域)时所产生的代码引起的。编译器会为我们生成析构函数,在该析构函数内,编译器去会安插代码调用 pImpl
,即一个使用了默认析构器的 std::unique_ptr
。默认析构器是在内部使用 delete
运算符针对裸指针执行析构的函数。然后,在 delete
之前,静态断言会确保裸指针未指向非完整类型。这样,w
的析构函数会遇到静态断言错误,从而编译失败。
为了解决这一问题,只要在生成析构 std::unique<Widget::Impl>
代码处, Widget::Impl
是个完整类型即可。成功编译的关键在于让编译器看到 WIdget
的析构函数的函数体的位置在 widget.cpp
内部的 Widget::Impl
定义之后。
实现方式是在 widget.h
内声明析构函数:
class Widget {
public:
Widget();
~Widget();
...
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
析构函数的定义应该在 Widget::Impl
定义之后:
#include "widget.h"
#include "gadget.h"
#include <string>
#include <vector>
struct Widget::Impl {
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};
Widget::WIdget()
: Pimp(std::make_unique<Impl>()) {}
Widget::~Widget() {}
使用了Pimpl习惯用法的类,自然支持移动操作。但是声明析构函数会阻止编译器生成移动操作,所以需要自己声明该函数。同时移动也会带来析构问题,所以必须将移动操作的代码放在实现文件的 Widget::Impl
定义之后:
class Widget {
public:
Widget();
~Widget();
Widget(Widget&& rhs);
Widget& operator=(Widget&& rhs);
...
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
#include "widget.h"
#include "gadget.h"
#include <string>
#include <vector>
struct Widget::Impl {
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};
Widget::WIdget()
: Pimp(std::make_unique<Impl>()) {}
Widget::~Widget() = default;
Widget::Widget(Widget&& rhs) = default;
Widget& Widget::operator=(Widget&& rhs) = default;
在实现复制时,要注意深拷贝:
class Widget {
public:
...
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs);
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
#include "widget.h"
...
struct Widget::Impl {...};
Widget::~Widget() = default;
Widget::Widget(const Widget& rhs)
: pImpl(std::make_unique<Impl>(*rhs.pImpl)) {}
Widget& Widget::operator=(const Widget& rhs)
{
*pImpl = *rhs.pImpl;
return *this;
}
对于Pimpl的习惯用法,使用 std::unique_ptr
实现对相应资源的专属所有权。如果在这里使用 std::shared_ptr
来实现 pImpl
,则本条款的建议不再适用,无需在 Widget
中声明析构函数。
原因是 std::unique_ptr
的析构器是类型的一部分,使得效率更高,但是要求指向的类型必须是完整类型。但对于 std::shared_ptr
,析构器并非智能指针类型的一部分,这就需要效率更低的目标代码,不要求指向的类型必须是完整类型。