Epipolar a personal journal

Pointer to Implementation

在 C++ 开发中有一个重要的技巧叫做 Pointer to Implementation (pimpl) 。它被用来隔离接口类和实现类。通常实现类中会包含一些对第三方二进制代码的依赖。而接口类的头文件中不会出现任何与第三方二进制有关的成员。这样一来便使得包含了头文件的用户代码永远不需要知道第三方二进制代码的存在。当这些第三方二进制发生改动时,只要接口类本身的接口(以及头文件)没有任何变动,用户代码侧就不需要重编译。这样就一定程度实现了 ABI 层的兼容。

举个例子,我们可能会有下面的代码:

struct Interface {
    void foo();
    OtherClass m_other;
};

这个接口类在析构时需要考虑到 OtherClass 的内存布局,那么当 OtherClass 发生了二进制级别的变化时,Interface 就不再兼容这些变化了。

我们可以采用下面的方法隔离开它:

class OtherClass;
struct Interface {
    void foo();
    OtherClass *m_pother;
};

由于 m_pother 是一个指针,它的大小是固定的,内存布局也是确定的,因此就无需关心 OtherClass 的具体实现。这样的指针就叫做 pimpl 。

这个实现类需要进行分配和释放,所以问题就多了一些,我们需要在接口类中添加构造和析构函数,并且它们要定义在接口类的实现(.cpp)中。这是因为如果在头文件里直接定义,就需要添加到第三方代码的头文件,于是用户代码里就会出现依赖,这是我们不希望看到的。

随着 C++ 的发展,智能指针也开始普及。利用智能指针进行资源管理有着简单安全的优点,所以一定会有人尝试用智能指针来管理这个 pimpl ,就像下面这样:

class OtherClass;
struct Interface {
    void foo();
    std::unique_ptr<OtherClass> m_pother;
};

但是这么做存在一个问题:std::unique_ptr 需要知道 OtherClass 的析构函数。上面这个接口类会交由编译器自动生成析构函数,在用户的代码里,我们没有 OtherClass 的完整定义,因此 std::unique_ptr 的析构无法生成,也就导致 Interface 不能自动生成析构函数。因此我们不能让编译器自动生成析构函数。

如果要使用智能指针完成 pimpl ,需要像下面这样:

class OtherClass;
struct Interface {
    ~Interface();
    void foo();
    std::unique_ptr<OtherClass> m_pother;
};

在类的接口定义中显式声明析构函数,然后在它的实现代码里:

Interface::~Interface() = default; // 或者加入你需要的析构过程

后记(2017-11-12):当然构造函数也要类似处理,否则 C++ 找不到实现类的定义,会产生编译错误。当然,这比运行时出现隐蔽的问题要容易发现。