首页 > 编程笔记 > C++笔记 阅读:2

C++实现pimpl惯用法(非常详细)

pimpl 代表指向实现的指针(也以柴郡猫惯用法或编译器防火墙惯用法知名),是用来将实现细节从接口中分离的不透明指针技术。好处是改变实现不需要修改接口,因此可避免重新编译使用此接口的代码。当只有实现细节变化时,库使用 pimpl 惯用法在其 ABI 上,可使其与老版本向后兼容。

在本节中,我们将看到如何使用现代 C++ 特性实现 pimpl 惯用法。为了以实际示例展示 pimpl 惯用法,我们考虑以下类,然后将其以 pimpl 惯用法重构:
class control
{
    std::string text;
    int width = 0;
    int height = 0;
    bool visible = true;
    void draw()
    {
        std::cout
            << "control " << '\n'
            << " visible: " << std::boolalpha << visible << std::noboolalpha << '\n'
            << " size: " << width << ", " << height << '\n'
            << " text: " << text << '\n';
    }
public:
    void set_text(std::string_view t)
    {
        text = t.data();
        draw();
    }

    void resize(int const w, int const h)
    {
        width = w;
        height = h;
        draw();
    }

    void show()
    {
        visible = true;
        draw();
    }

    void hide()
    {
        visible = false;
        draw();
    }
};
这个类表示控制台,有文本、大小和可见性的属性。每当这些属性变化时,控制台重绘。在这模拟的实现中,绘画意味着将属性的值输出到控制台上。

pimpl惯用法的实现过程

遵循以下步骤来实现pimpl惯用法,通过重构之前显示的 control 类来举例说明:

1) 将所有私有成员(数据和函数)放到不同的类中。我们称其为 pimpl 类,原始的类为公共类。

2) 在公共类的头文件中,放置 pimpl 类的前置声明:
// in control.h
class control_pimpl;

3) 在公共类定义中,使用 unique_ptr 声明指向 pimpl 类的指针。这应该是这个类唯一的私有数据成员:
class control
{
    std::unique_ptr<control_pimpl, void(*)(control_pimpl*)> pimpl;
public:
    control();
    void set_text(std::string_view text);
    void resize(int const w, int const h);
    void show();
    void hide();
};

4) 将 pimpl 类定义放在公共类的源文件中。pimpl 类模仿公共类的公共接口:
// in control.cpp
class control_pimpl
{
    std::string text;
    int width = 0;
    int height = 0;
    bool visible = true;

    void draw()
    {
        std::cout
            << "control " << '\n'
            << " visible: " << std::boolalpha << visible
            << std::noboolalpha << '\n'
            << " size: " << width << ", " << height << '\n'
            << " text: " << text << '\n';
    }
public:
    void set_text(std::string_view t)
    {
        text = t.data();
        draw();
    }

    void resize(int const w, int const h)
    {
        width = w;
        height = h;
        draw();
    }

    void show()
    {
        visible = true;
        draw();
    }

    void hide()
    {
        visible = false;
        draw();
    }
};

5) pimpl 类在在公共类的构造函数中实例化:
control::control() :
    pimpl(new control_pimpl(),
          [](control_pimpl* pimpl) {delete pimpl; })
{}

6) 公共类成员函数调用相应的 pimpl 类的成员函数:
void control::set_text(std::string_view text)
{
    pimpl->set_text(text);
}

void control::resize(int const w, int const h)
{
    pimpl->resize(w, h);
}

void control::show()
{
    pimpl->show();
}

void control::hide()
{
    pimpl->hide();
}

深度剖析pimpl惯用法

pimpl 惯用法使类的内部实现能够向该类所属的库或模块的客户端隐藏。这有几个好处:
以上提及的好处不是免费的,有几个缺点需要提及:

作为一条经验法则,当实现 pimpl 惯用法时,总是将除了虚函数以外的所有私有数据和函数成员放到 pimpl 类,将保护数据成员与函数和所有私有虚函数放到公共类中。

在本节示例中,control_pimpl 类基本上和原始 control 类一样。实际上,类越大(包括虚函数和保护数据,以及函数和成员),pimpl 类和非 pimpl 类不是完全相同的。而且实际上,pimpl 类可能需要有指向公共类的指针,以便调用没有移动到 pimpl 类上的成员。

关于重构 control 类的实现,指向 control_pimpl 对象的指针由 unique_ptr 管理。对于这个指针的声明,我们使用自定义删除器:
std::unique_ptr<control_pimpl, void(*)(control_pimpl*)> pimpl;
这么做的原因是 control 类有个由编译器默认定义的析构函数,某种程度上 control_pimpl 类还是不完整的(即在头文件中)。

使用 unique_ptr 时会有错误,无法删除不完整类型。这个问题可通过两种方法解决:

相关文章