条款31: 将文件间的编译依存关系降到最低。 — 《Effective C++》

条款22: 使用pImpl习惯用法时,将特殊成员函数的定义放到实现文件中。 — 《Effective Modern C++》

pImpl习惯用法,即pointer to implementation,指的是以指针的方式访问类的实现部分。

比如:

class Widget {
public:
    Widget();
    //...
private:
    std::string name;
    std::vector<double> data;
};

在这个类定义中,因为数据成员的缘故,这个头文件里面需要包含string,vector等,进而导致凡是使用了这个类的地方都会需要编译一次。

这不但提高了整体的编译时间,而且因为依赖问题一旦有任何改变都需要重新编译一次。

pImpl习惯用法可以降低依赖以提升编译速度,如:

// Widget.hpp

class Widget{
public:
    Widget();
    ~Widget();
    //...
private:
    struct Impl;  // 用一个结构体来存储实现部分
    Impl *pImpl;  // 用一个指针来访问实现部分
};

Widget::Impl是一个内部的非完整类型,而这里的技巧就在于用一个指针来指向它,从而不需要包含string等头文件,也不会因为依赖问题导致跟随着重新编译。

在实现部分来处理类的成员:

// Widget.cpp

struct Widget::Impl
{
    std::string name;
    std::vector<double> data;
};

Widget()
: pImpl(new Impl)
{
}

~Widget()
{
    delete pImpl;
}

// main.cpp

#include "Widget.hpp"

int main()
{
    Widget w;
    return 0;
}

但我们想使用智能指针来处理,而不是直接使用“裸”指针,也不想出现new和delete语句:

// Widget.hpp

#include <memory>

class Widget {
public:
    Widget();
private:
    struct Impl;
    std::unique_ptr<Impl> pImpl;
};

// Widget.cpp

#include "Widget.hpp"

#include <string>
#include <vector>

struct Widget::Impl
{
    std::string name;
    std::vector<double> data;
};

Widget::Widget()
: pImpl(std::make_unique<Impl>())
{
}

但因为使用了unique_ptr智能指针,它会因为Impl在头文件中是一个非完整类型而报错,原因是编译器自动产生的一些默认代码时还不知道Impl的完整定义,需要做些改动:

// Widget.hpp

#include <memory>

class Widget {
public:
    Widget();
    ~Widget(); // 不使用编译器自动生成的析构函数
private:
    struct Impl;
    std::unique_ptr<Impl> pImpl;
};

// Widget.cpp
#include "Widget.hpp"

#include <string>
#include <vector>

struct Widget::Impl
{
    std::string name;
    std::vector<double> data;
};

Widget::Widget()
: pImpl(std::make_unique<Impl>())
{
}

Widget::~Widget() = default;   // 还是使用编译自动产生的默认析构函数,但此时已经知道Impl的完整定义了。

这样就可以顺利编译了。

但因为unique_ptr要求类型是完整类型,相应的需要把一些特殊函数进行声明和实现,这样才能正确执行移动、复制等操作:

// Widget.hpp

#include <memory>

class Widget {
public:
    Widget();
    ~Widget();   // 析构函数

    Widget(Widget&& rhs);  // 移动构造函数
    Widget& operator=(Widget&& rhs); // 移动复制运算符

    Widget(const Widget& rhs);   // 复制构造函数 
    Widget& operator=(const Widget& rhs); // 复制赋值运算符

private:
    struct Impl;
    std::unique_ptr<Impl> pImpl;
};

// Widget.cpp

#include "Widget.hpp"

#include <string>
#include <vector>

struct Widget::Impl
{
    std::string name;
    std::vector<double> data;
};

Widget::Widget()
: pImpl(std::make_unique<Impl>())
{
}

Widget::~Widget() = default;

Widget::Widget(Widget&& rhs) = default;
Widget& Widget::operator=(Widget&& rhs) = default;

Widget::Widget(const Widget& rhs)
: pImpl(std::make_unique<Impl>(*rhs.pImpl))
{
}

Widget& Widget::operator=(const Widget& rhs)
{
   *pImpl = *rhs.pImpl;
   return *this;
}

测试:

// main.cpp

#include "Widget.hpp"

int main()
{
    Widget w;

    Widget w1(w);
    Widget w2 = w;

    auto w3(std::move(w));
    auto w4 = std::move(w3);

    return 0;
}

基本上在采用pImpl习惯用法时,首选使用unique_ptr智能指针,进而必须实现类的五个特殊函数:

  • 析构函数
  • 移动构造函数
  • 移动赋值运算符
  • 复制构造函数
  • 复制赋值运算符

采用pImpl习惯用法不但加快编译速度,而且做到接口和实现的真正分离,尽可能采用此用法。