PIMPL

ZaynPei Lv6

PIMPL (Pointer to Implementation) - 指向实现的指针, 也被称为“编译防火墙 (Compilation Firewall)”或“不透明指针 (Opaque Pointer)”。它的核心思想是将类的私有成员和私有方法头文件中完全移除,放到一个独立的、只在 .cpp 文件中定义的实现类(Impl)中。头文件里只保留一个指向该实现类的指针

通过将类的接口与其实现细节分离,可以解决以下问题:

  • 降低编译依赖,大幅加快编译速度:这是 PIMPL 最主要的目的。通常,如果一个类的头文件包含了其他头文件(例如 , ),那么任何包含了这个类头文件的 .cpp 文件,都会间接地依赖那些头文件。当你修改类的私有成员时(比如把 std::vector 换成 std::list),所有包含了该类头文件的文件都必须重新编译。在大型项目中,这可能意味着几分钟甚至几十分钟的编译时间。使用 PIMPL 后,所有实现细节和依赖的头文件都转移到了 .cpp 文件中。你修改私有实现时,只需要重新编译这个类自己的 .cpp 文件,链接器会处理好剩下的事情,从而极大地提升了编译效率。

  • 隐藏实现细节:头文件只暴露公有接口,所有内部数据结构、使用的第三方库等都被完全隐藏。这对于发布二进制库(SDK)的开发者来说非常有用。

  • 提供稳定的ABI (Application Binary Interface):只要公有接口不变,你就可以在不破坏ABI兼容性的前提下,随意修改 Impl 类的内部实现(增删私有成员)。这意味着用户无需重新编译他们的代码,就可以直接使用新版本的动态库/静态库。

假设我们有一个 Widget 类, 没有使用 PIMPL的情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Widget.h
#pragma once
#include <string> // 编译依赖
#include <vector> // 编译依赖

class Widget {
public:
Widget();
void do_something();

private:
std::string m_name;
std::vector<int> m_data;
// 任何包含 Widget.h 的文件都会被迫包含 <string> 和 <vector>
// 修改任何私有成员,所有包含者都需重编
};

使用 PIMPL 的版本 (After):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Widget.h
#pragma once
#include <memory> // 只需要包含智能指针

class Widget {
public:
Widget();
~Widget(); // 关键:必须在头文件中声明析构函数

// 移动构造和移动赋值(可选,但推荐)
Widget(Widget&&);
Widget& operator=(Widget&&);

// 拷贝构造和拷贝赋值(如果需要)
Widget(const Widget&);
Widget& operator=(const Widget&);

void do_something();

private:
class Impl; // 关键:前向声明实现类,不需要知道它的细节
std::unique_ptr<Impl> m_pimpl; // 关键:一个指向实现的指针
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// Widget.cpp
#include "Widget.h"
#include <string> // 实现所依赖的头文件只在这里包含
#include <vector> // 不会暴露给外部
#include <iostream>

// 关键:在这里完整定义实现类 Impl
class Widget::Impl {
public:
void do_something_impl() {
std::cout << "Doing something with " << m_name << std::endl;
m_data.push_back(1);
}

private:
std::string m_name;
std::vector<int> m_data;
};

// 关键:构造函数和析构函数必须在 Impl 类完全定义之后再定义
Widget::Widget() : m_pimpl(std::make_unique<Widget::Impl>()) {}

// 关键:析构函数必须在这里定义,否则 std::unique_ptr 会因为 Impl 类型不完整而出错
Widget::~Widget() = default;

// 其他成员函数只是简单地将调用转发给 Impl 对象
void Widget::do_something() {
m_pimpl->do_something_impl();
}

// 移动和拷贝构造/赋值也需要在这里实现
Widget::Widget(Widget&&) = default;
Widget& Widget::operator=(Widget&&) = default;

// 拷贝构造需要深拷贝
Widget::Widget(const Widget& other)
: m_pimpl(std::make_unique<Impl>(*other.m_pimpl)) {}

Widget& Widget::operator=(const Widget& other) {
if (this != &other) {
*m_pimpl = *other.m_pimpl;
}
return *this;
}

从中我们可以看到,Widget 类的头文件中不再包含任何实现细节和依赖的头文件。所有私有成员都被移到了 Impl 类中,并且 Impl 类只在 Widget.cpp 中定义。 这样,当我们修改 Impl 类的私有成员时,只有 Widget.cpp 需要重新编译,其他包含 Widget.h 的文件不受影响,从而大大提升了编译效率

总之, PIMPL 是一种设计模式,它利用 RAII 技术来管理其实现对象的生命周期。在我们的 PIMPL 示例中,std::unique_ptr<Impl> m_pimpl; 就是这种关系的体现。std::unique_ptr 是一个 RAII 包装器, 它包装的资源是堆上分配的 Impl 对象。当 Widget 对象被销毁时,它的成员 m_pimpl 也会被销毁。m_pimpl 的析构函数会自动 delete 它所拥有的 Impl 对象,完美地实现了资源的自动管理。

On this page
PIMPL