Skip to content

Latest commit

 

History

History
182 lines (142 loc) · 4.53 KB

File metadata and controls

182 lines (142 loc) · 4.53 KB

条款22:使用Pimpl习惯用法时,将特殊成员函数的定义放到实现文件中

考虑 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 ,析构器并非智能指针类型的一部分,这就需要效率更低的目标代码,不要求指向的类型必须是完整类型。