PImpl 技术(编译防火墙)PImpl 技术的智能指针版本
Effective Modern C++ Item 22 的学习和解读。
这部分介绍一个智能指针的应用场景:PImpl技术,俗称编译防火墙。
PImpl 技术(编译防火墙)PImpl(Pointer to implementation)是一种 C++ 惯用技术,它是通过将类的具体实现放在另一个单独的类(或结构体)中,并通过不透明的指针进行访问。该技术能够将具体的实现细节从对象中去除,能够有效地减少编译依赖。也被称为“编译防火墙(Compilation Firewalls)”。看一个例子:
class Widget { // in header "widget.h"
public:
Widget();
...
private:
std::string name;
std::vector data;
Gadget g1, g2, g3; // Gadget is some user-defined type
};
这里, Widget 中包含 std::string、std::vector 和 Gadget 类型成员变量,对于 Widget 的客户,必须包含
PImpl 技术可以很好的解决这个问题。将 Widget 类中的数据成员变量封装成一个类(或结构体),然后将数据成员替换成这个类(或结构体)的指针。
class Widget { // still in header "widget.h"
public:
Widget();
~Widget(); // dtor is needed—see below
...
private:
struct Impl; // declare implementation struct
Impl *pImpl; // and pointer to it
};
Widget 类中不在提及 std::string、std::vector 和 Gadget 类型,因而无需包含相应的头文件。即使这些头文件的内容发生改变, Widget 客户代码也无需重新编译。
一个只声明不定义的类型被成为不完整类型(incomplete type),Widget::Impl 就是这样的一个类型,声明一个指向它的指针是可以编译的,PImpl 技术就是利用了这一点。PImpl 技术的典型方式如下:
前向声明一个类型,然后申明一个指向这个类型的指针。在原始类的实现文件中定义这个类型,并实现这个指针的动态内存分配和回收。
#include "widget.h" // in impl. file "widget.cpp" #include "gadget.h" #include#include struct Widget::Impl { std::string name; // definition of Widget::Impl std::vector data; // with data members formerly in Widget Gadget g1, g2, g3; }; Widget::Widget() // allocate data members for : pImpl(new Impl) // this Widget object {} Widget::~Widget() // destroy data members for { delete pImpl; } // this object
这样,就把
以上就是 Pimpl 技术的基本原理,这里都是直接使用原始指针,完全是 C++98 风格的实现,C++11 之后,我们更倾向使用智能指针来代替原始指针。
PImpl 技术的智能指针版本这里,std::unique_ptr 是比较合适用来替换原始指针的,我们修改上面的代码:
// in "widget.h" #includeclass Widget { public: Widget(); ... private: struct Impl; std::unique_ptr pImpl; // use smart pointer instead of raw pointer }; //==================================================================================// // in "widget.cpp" #include "widget.h" #include "gadget.h" #include #include struct Widget::Impl { // as before std::string name; std::vector data; Gadget g1, g2, g3; }; Widget::Widget() : pImpl(std::make_unique ()) {}
由于智能指针的自动析构指向的资源,这里无需析构函数。
#include "widget.h"
int main() {
Widget w;
return 0;
}
但是,以上代码会编译报错,报错信息如下:
In file included from /usr/include/c++/9/memory:80,
from widget.h:3,
from hello.cpp:1:
/usr/include/c++/9/bits/unique_ptr.h: In instantiation of ‘void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = Widget::Impl]’:
/usr/include/c++/9/bits/unique_ptr.h:292:17: required from ‘std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = Widget::Impl; _Dp = std::default_delete]’
widget.h:5:7: required from here
/usr/include/c++/9/bits/unique_ptr.h:79:16: error: invalid application of ‘sizeof’ to incomplete type ‘Widget::Impl’
79 | static_assert(sizeof(_Tp)>0,
|
编译报错分析如下:
在 main 函数结束时候,离开了 w 的作用域,w 将被销毁,也即会调用 Widget 的析构函数。我们的代码虽然没有定义 Widget 的析构函数,但是根据 Item 17 的介绍可以知道,编译器会生成默认的析构函数(inline 的)。析构函数中会释放 pImpl,pImpl 的是 std::unique_ptr
/// Primary template of default_delete, used by unique_ptr templatestruct default_delete { /// Default constructor constexpr default_delete() noexcept = default; template ::value>::type> default_delete(const default_delete<_Up>&) noexcept { } /// Calls @c delete @p __ptr void operator()(_Tp* __ptr) const { static_assert(!is_void<_Tp>::value, "can't delete pointer to incomplete type"); static_assert(sizeof(_Tp)>0, "can't delete pointer to incomplete type"); delete __ptr; } };
所以,要解决这个问题,只需要保证在销毁 std::unique_ptr
// in "widget.h" #includeclass Widget { public: Widget(); ~Widget(); // declaration only ... private: struct Impl; std::unique_ptr pImpl; // use smart pointer instead of raw pointer }; //==================================================================================// // in "widget.cpp" #include "widget.h" #include "gadget.h" #include #include struct Widget::Impl { // as before std::string name; std::vector data; Gadget g1, g2, g3; }; Widget::Widget() : pImpl(std::make_unique ()) {} Widget::~Widget() {} // ~Widget definition // Widget::~Widget() = default; // same effect as above
由于智能指针的自动析构指向的资源,这里无需析构函数。
#include "widget.h"
int main() {
Widget w;
return 0;
}
这样就可以编译通过了。但是上面的实现还有点问题:从 Item 17 介绍我们知道,析构函数的声明会阻止编译器生成 move 操作,那么以上代码将不支持 move 操作了。解决办法也是相似的:
// in "widget.h" #includeclass Widget { public: Widget(); ~Widget(); // declaration only Widget(Widget&& rhs); // declarations Widget& operator=(Widget&& rhs); // only ... private: struct Impl; std::unique_ptr pImpl; // use smart pointer instead of raw pointer }; //==================================================================================// // in "widget.cpp" #include "widget.h" #include "gadget.h" #include #include struct Widget::Impl { // as before std::string name; std::vector data; Gadget g1, g2, g3; }; Widget::Widget() : pImpl(std::make_unique ()) {} Widget::~Widget() {} // ~Widget definition // Widget::~Widget() = default; // same effect as above Widget::Widget(Widget&& rhs) = default; Widget& Widget::operator=(Widget&& rhs) = default;
这样 Widget 具备了 move 操作。但是还是存在一点问题:
根据 Item 17 知道,因为自定义了 move 操作,将会阻止编译器生成 copy 操作。即使编译器生成了 copy 操作(使用 = default 进行声明),也是一个浅拷贝,std::uniqe_ptr 是一个所有权独享的对象,对它进行拷贝会转移所有权。
因此,需要我们自定义 copy 操作:
// in "widget.h" #includeclass Widget { public: Widget(); ~Widget(); // declaration only Widget(Widget&& rhs); // declarations Widget& operator=(Widget&& rhs); // only Widget(const Widget& rhs); // declarations Widget& operator=(const Widget& rhs); // only ... private: struct Impl; std::unique_ptr pImpl; // use smart pointer instead of raw pointer }; //==================================================================================// // in "widget.cpp" #include "widget.h" #include "gadget.h" #include #include struct Widget::Impl { // as before std::string name; std::vector data; Gadget g1, g2, g3; }; Widget::Widget() : pImpl(std::make_unique ()) {} Widget::~Widget() {} // ~Widget definition // Widget::~Widget() = default; // same effect as above Widget::Widget(Widget&& rhs) = default; Widget& Widget::operator=(Widget&& rhs) = default; Widget::Widget(const Widget& rhs) // copy ctor : pImpl(std::make_unique (*rhs.pImpl)) {} Widget& Widget::operator=(const Widget& rhs) // copy operator= { *pImpl = *rhs.pImpl; return *this; }
到目前为止,以上代码的实现是比较完整的了。
为了实现 Pimpl 技术,std::unique_ptr 是合适的,因为 pImpl 指针对 Impl 有独有所有权。如果你使用 std::shared_ptr 代替 std::unique_ptr,以上出现的问题将不会出现。示例如下:
// in "widget.h" #includeclass Widget { public: Widget(); ... private: struct Impl; std::shared_ptr pImpl; }; // in "widget.cpp" #include "widget.h" #include "gadget.h" #include #include struct Widget::Impl { // as before std::string name; std::vector data; Gadget g1, g2, g3; }; Widget::Widget() : pImpl(std::make_shared ()) {} //======================================================// Widget w1; auto w2(std::move(w1)); // move-construct w2 w1 = std::move(w2); // move-assign w1
std::shared_ptr 的 deleter 不是其自身的一部分,属于控制块,我们的代码不会包含删除器的代码,因此不需要自定义析构函数,那么 move 和 copy 操作都会自定生成。而 std::shared_ptr 又是值语义的,拷贝也不会发生问题(通过引用计数进行内存管理)。



