实现 pimpl 惯用语

PIMPL代表“指向具体实现的指针”(但也称为Cheshire cat惯用语或编译器防火墙惯用语),并且是一种不透明的指针技术,可将实现细节从接口中分离出来。这样做的好处是无需更改接口就可以更改具体实现,因此可以避免需要重新编译使用了某个接口的代码。当仅更改实现细节时,这就有可能使库在其ABI上使用pimpl惯用语,而pimpl惯用语与旧版本向后兼容。在本文中,我们将看到如何使用现代C ++功能实现pimpl惯用语。

做好准备

希望读者熟悉本文前面各章中讨论的智能指针和std::string_view。

为了以实践的方式演示pimpl惯用语,我们将考虑以下类,然后将按照pimpl模式进行重构。该类表示具有诸如text,size和visibility之类的属性的控件。每次更改这些属性时,都会重新绘制控件(在此模拟实现中,绘制意味着将属性的值打印到控制台):

class control
{
  std::string text;
  int width = 0;
  int height = 0;
  bool visible = true;

  void draw()
  {
    std::cout 
      << "control " << std::endl
      << " visible: " << std::boolalpha << visible << 
         std::noboolalpha << std::endl
      << " size: " << width << ", " << height << std::endl
      << " text: " << text << std::endl;
  }
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惯用语,此处通过重构前面显示的控件类来举例说明:

  1. 将所有私有成员(数据和函数)放入单独的类。 我们将其称为pimpl类,将原始类称为public类。
  2. 在公共类的头文件中,向pimpl类添加前向声明:
// in control.h
class control_pimpl;
  1. 在公共类定义中,使用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();
};
  1. 将pimpl类定义放在公共类的源文件中。 pimpl类反映了public类的public接口:
// in control.cpp
class control_pimpl
{
  std::string text;
  int width = 0;
  int height = 0;
  bool visible = true;

  void draw()
  {
     std::cout
       << "control " << std::endl
       << " visible: " << std::boolalpha << visible 
       << std::noboolalpha << std::endl
       << " size: " << width << ", " << height << std::endl
       << " text: " << text << std::endl;
  }

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();
  }
};
  1. pimpl类在public类的构造函数中实例化:
control::control() :
  pimpl(new control_pimpl(),
        [](control_pimpl* pimpl) {delete pimpl; })
{}
  1. 公共类成员函数调用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惯用语时,始终将所有私有成员数据和函数(虚拟函数除外)放在pimpl类中,并将受保护的数据成员和函数以及所有私有虚拟函数保留在public类中。

在本文的示例中,control_pimpl类与原始控件类基本相同。实际上,在类较大的地方,具有虚函数和受保护的成员,以及函数和数据,pimpl类并不完全等同于如果未实现该类的情况。同样,在实践中,pimpl类可能需要指向公共类的指针才能调用未移入pimpl类的成员。

关于重构控制类的实现,指向control_pimpl对象的指针由unique_ptr管理。在此指针的声明中,我们使用了自定义删除器:

std::unique_ptr<control_pimpl, void(*)(control_pimpl*)> pimpl;

这样做的原因是,在control_pimpl类型仍然不完整(即在标头中)的点,控件类具有由编译器隐式定义的析构函数。这将导致unique_ptr错误,无法删除不完整的类型。 该问题可以通过两种方式解决:

  • 在control_pimpl类的完整定义可用之后,为显式实现的控制类提供用户定义的析构函数(即使声明为默认值)。
  • 就像我们在此示例中所做的那样,为unique_ptr提供自定义删除器。

还有更多……

原始控件类既可以复制又可以移动:

control c;
c.resize(100, 20);
c.set_text("sample");
c.hide();

control c2 = c;             // copy
c2.show();

control c3 = std::move(c2); // move
c3.hide();

重构的控件类只能移动,不能复制。为了使其既可复制又可移动,我们必须提供复制构造函数和复制赋值运算符,以及移动构造函数和移动赋值运算符。可以默认使用后一个对象,但必须显式实现前一个对象,以便从复制对象创建新的control_pimpl对象。以下代码显示了可复制和可移动控件类的实现:

class control_copyable
{
  std::unique_ptr<control_pimpl, void(*)(control_pimpl*)> pimpl;
public:
  control_copyable();
  control_copyable(control_copyable && op) noexcept;
  control_copyable& operator=(control_copyable && op) noexcept;
  control_copyable(const control_copyable& op);
  control_copyable& operator=(const control_copyable& op);

  void set_text(std::string_view text);
  void resize(int const w, int const h);
  void show();
  void hide();
};

control_copyable::control_copyable() :
  pimpl(new control_pimpl(),
        [](control_pimpl* pimpl) {delete pimpl; })
{}

control_copyable::control_copyable(control_copyable &&) 
   noexcept = default;
control_copyable& control_copyable::operator=(control_copyable &&) 
   noexcept = default;

control_copyable::control_copyable(const control_copyable& op)
   : pimpl(new control_pimpl(*op.pimpl),
           [](control_pimpl* pimpl) {delete pimpl; })
{}

control_copyable& control_copyable::operator=(
   const control_copyable& op) 
{
  if (this != &op) 
  {
    pimpl = std::unique_ptr<control_pimpl,void(*)(control_pimpl*)>(
               new control_pimpl(*op.pimpl),
               [](control_pimpl* pimpl) {delete pimpl; });
  }
  return *this;
}

// the other member functions

参见

  • 使用unique_ptr独占内存资源

发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据