Skip to content

Latest commit

 

History

History
169 lines (123 loc) · 7 KB

File metadata and controls

169 lines (123 loc) · 7 KB

七、桥接

如果你一直在关注 C++ 编译器的最新进展(特别是 GCC、Clang 和 MSVC),你可能已经注意到编译速度在提高。特别是,编译器变得越来越增量,以至于编译器实际上只能重建已经改变的定义,并重用其余的,而不是重建整个翻译单元。

我提出 C++ 编译的原因是因为“一个奇怪的技巧”(又是那个短语!)一直被开发人员用来尝试和优化编译速度。

当然,我说的是…

通俗的习语

让我首先解释一下在 Pimpl 习语中发生的技术方面的事情。假设您决定创建一个Person类,它存储一个人的名字并允许他们打印问候。不是像通常那样定义Person的成员,而是像这样定义类:

 1   struct Person
 2   {
 3     std::string name;
 4     void greet();
 5
 6     Person();
 7     ~Person();
 8
 9     class PersonImpl;
10     PersonImpl *impl; // good place for gsl::owner<T>
11   };

这太奇怪了。对于一个简单的类来说,似乎有很多工作要做。让我们看看…我们有了名字和greet()函数,但是为什么还要麻烦构造器和析构函数呢?还有这个class PersonImpl是什么?

您看到的是一个选择在另一个类中隐藏其实现的类,这个类被称为PersonImpl。需要特别注意的是,这个类没有在头文件中定义,而是驻留在.cpp文件中(Person.cpp,所以PersonPersonImpl在同一位置)。它的定义非常简单

1   struct Person::PersonImpl
2   {
3     void greet(Person* p);
4   }

最初的Person类向前声明PersonImpl,并继续保存指向它的指针。正是这个指针在Person's构造器中被初始化,在析构函数中被销毁;如果智能指针能让你感觉更好,请随意使用。

1   Person::Person()
2     : impl(new PersonImpl) {}
3
4   Person::~Person() { delete impl; }

现在,我们开始实现Person:: greet(),你可能已经猜到了,它只是将控制权传递给了PersonImpl::greet():

1   void Person::greet()
2   {
3     impl->greet(this);
4   }
5
6   void Person::PersonImpl::greet(Person* p)
7   {
8     printf("hello %s", p->name.c_str());
9   }

所以…简而言之,这就是 Pimpl 的习惯用法,所以唯一的问题是为什么?!?为什么要麻烦地跳过所有的关卡,委派greet()并传递一个this指针呢?这种方法有三个优点:

  • 更大比例的类实现实际上是隐藏的。如果您的Person类需要一个充满了private/protected成员的丰富 API,那么您将向您的客户公开所有这些细节,即使由于private/protected访问修饰符,他们永远无法访问这些成员。使用 Pimpl,它们只能被提供公共接口。
  • 修改隐藏 Impl 类的数据成员不会影响二进制兼容性。
  • 头文件只需要包含声明所需的头文件,而不需要包含实现。例如,如果Person需要一个类型为vector<string>的私有成员,您将被迫在Person.h头中对<vector><string>都进行#include(这是可传递的,所以任何使用Person.h的人也会包括它们)。使用 Pimpl 习惯用法,这可以在.cpp文件中完成。

您会注意到,上述几点使我们能够保持一个干净的、不变的头文件。这样做的副作用是降低了编译速度。而且,对我们来说重要的是,Pimpl 实际上是桥模式的一个很好的例子:在我们的例子中,pimpl opaque 指针(opaque 是 transparent 的反义词,也就是说,你不知道它后面是什么)作为一个桥,将公共接口的成员和隐藏在.cpp文件中的底层实现粘合在一起。

Pimpl 习惯用法是桥设计模式的一个非常具体的说明,所以让我们来看一些更一般的东西。假设我们有两类对象(在数学意义上):几何形状和可以在屏幕上绘制它们的渲染器。

就像我们对适配器模式的说明一样,我们将假设呈现可以以矢量和光栅的形式发生(尽管我们不会在这里编写任何实际的绘图代码),就形状而言,让我们只限于圆形。

首先,这里是Renderer基类:

1   struct Renderer
2   {
3     virtual void render_circle(float x, float y, float radius) = 0;
4   };

我们可以很容易地构造矢量和光栅实现;我将使用一些代码模拟下面的实际渲染,以将内容写入控制台:

 1   struct VectorRenderer : Renderer
 2   {
 3     void render_circle(float x, float y, float radius) override

 4     {
 5       cout << "Rasterizing circle of radius " << radius << endl;
 6     }
 7   };
 8
 9   struct RasterRenderer : Renderer
10   {
11     void render_circle(float x, float y, float radius) override

12     {
13       cout << "Drawing a vector circle of radius " << radius << endl;
14     }
15   };

基类Shape将保持对渲染器的引用;该形状将支持使用draw()成员函数的自呈现,还将支持resize()操作:

1   struct Shape
2   {
3   protected:
4     Renderer& renderer;
5     Shape(Renderer& renderer) : renderer{ renderer } {}
6   public:
7     virtual void draw() = 0;
8     virtual void resize(float factor) = 0;
9   };

你会注意到Shape类引用了一个Renderer。这恰好是我们建造的桥梁。我们现在可以创建一个Shape类的实现,提供额外的信息,比如圆心的位置和半径。

 1   struct Circle : Shape
 2   {
 3     float x, y, radius;
 4
 5     void draw() override

 6     {
 7       renderer.render_circle(x, y, radius);
 8     }
 9
10     void resize(float factor) override

11     {
12       radius *= factor;
13     }
14
15    Circle(Renderer& renderer, float x, float y, float radius)
16      : Shape{renderer}, x{x}, y{y}, radius{radius} {}
17   };

好了,这个模式很快就暴露出来了,有趣的部分当然是在draw()中:这是我们使用桥将Circle(它有关于它的位置和大小的信息)连接到渲染过程的地方。而确切地说,这里的桥梁是一座Renderer,例如:

1   RasterRenderer rr;
2   Circle raster_circle{ rr, 5,5,5 };
3   raster_circle.draw();
4   raster_circle.resize(2);
5   raster_circle.draw();

在前面的例子中,桥是RasterRenderer:你创建它,将一个引用传入Circle,从那时起,对draw()的调用将使用这个RasterRenderer作为桥,画圆。如果你需要微调圆圈,你可以resize()它,渲染仍然会工作得很好,因为渲染器不知道也不关心Circle,甚至不把它作为参考!

摘要

桥是一个相当简单的概念,作为一个连接器或胶水,将两个部分连接在一起。抽象(接口)的使用允许组件在没有真正意识到具体实现的情况下相互交互。

也就是说,桥模式的参与者确实需要意识到彼此的存在。具体来说,Circle需要一个对Renderer的引用,相反,Renderer知道如何专门画圆(因此,draw_circle()成员函数的名字)。这与中介模式形成对比,中介模式允许对象在不直接知道对方的情况下进行通信。