如果你一直关注C++编译器(特别GCC, Clang和MSVC)的最新进展,那么可能已经注意到编译速度提高了。特别是,编译的体量越来越大时。因为,编译器实际上只会重新编译改动的代码,并复用已编译好的未改动的部分,而不是重新构建整个编译单元。
之所以提起C++编译器,是因为过去开发者们一直在使用一个奇怪的技巧来加快编译速度。
首先,我们从技术角度来解释下Pimpl模式。假如我们要定义一个Person类。其中保存了人物的名字,并且提供了打印问候语的方法。除了像平时那样定义Person的成员,我们还可以像下面这样定义类:
struct Person {
std::string name;
void greet();
Person();
~Person();
class PersonImpl;
PersonImpl *impl;
};
看起来很简单的类尽然搞出如此多的工作!我们仔细看看:Person中定义了namec成员和greet()方法,但为什么要定义构造函数和西沟函数呢?PersonImpl类又是干什么的?
我们看到的这个Person类将其具体实现隐藏在另一个类中,即PersonImpl。需要格外注意的是,PersonImpl类并不是在头文件中定义的,而是驻留在 .cpp 文件(person.cpp,因此Person和PersonImpl放在一起)。它的定义非常简单:
struct Person::PersonImpl {
void greet(Person *p);
};
类Person中对PersonImpl做了前向声明,并且保存了一个PersonImpl类型指针。我们在Person的构造函数中初始化这个指针,并且在析构函数中销毁它。如果我们习惯使用智能指针,也可以将其替换为智能指针。
Person::Person() : impl(new PersonImpl()) {
}
Person::~Person() {
delete impl;
}
现在,我们要实现Person::greet()接口,正如你预料的那样,这个接口只是把控制权交给PersonImpl::greet():
void Person::greet() {
impl->greet(this);
}
void Person::PersonImpl::greet(Person *p) {
printf("hello %s!\n", p->name.c_str());
}
简单来说,这就是Pimpl模式。因此,现在唯一的问题是:为什么?为什么要费尽周折实现greet()的代理并且传递this指针呢?这种方法有三个优点:
Pimlpl莫斯可以使系统中的头文件更加整洁,并且不必频繁改动。不过,副作用是会影响编译速度。就本章介绍的内容而言,Pimpl模式是桥接模式的一种很好的体现:在刚才的示例中,Pimpl是一种不透明指针(即我们不知道它背后是什么),它起着桥梁的作用,将公共接口的成员与其隐藏在 .cpp文件中的底层实现结合起来。
Pimpl模式是桥接(Bridge)模式的一种具体实现,现在我们看看关于桥接模式更加通用的做法。假设有两种(数学意义上的)对象:几何对象以及将几何对象绘制在屏幕上的渲染器对象。
如同我们在适配器模式中展示的那样,假设我们可以以向量和光栅形式进行渲染(尽管我们不会在这里编写任何实际的绘图代码),并且将几何对象的形状限制为圆形。
首先, 基类Render定义如下:
struct Render {
virtual void render_circle(float x, float y, float radius) = 0;
};
我们可以轻松的构建向量渲染和光栅渲染的具体实现,下面将编写一些打印到控制台的代码来模拟实际渲染过程:
struct VectorRender: Render
{
void render_circle(float x, float y, float radius) override {
std::cout << "\nDrawing a vector circle of radius " << radius << "\n";
}
};
struct RasterRender:Render
{
void render_circle(float x, float y, float radius) override {
std::cout << "\nRasterizing circle of radius " << radius << "\n";
}
};
几何对象的基类Shape可以保存一个渲染器的引用,我们在Shape中定义draw()个resize()两个成员函数用于支持渲染和调整尺寸的操作:
struct Shape
{
protected:
Render& render;
Shape(Render& render) : render(render) {}
public:
virtual void draw() = 0;
virtual void resize(float factor) = 0;
};
可以看到,Shape类含有一个Render类型的引用。这正是桥接模型的“桥”之所在。接下来,我们创建Shape类的一个具体实现,并添加诸如圆心位置和半径等更多信息。
struct Circle: Shape
{
float x;
float y;
float radius;
Circle(Render& render, float x, float y, float radius)
:Shape(render), x(x), y(y), radius(radius) {}
void draw() override {
render.render_circle(x, y, radius);
}
void resize(float factor) override {
radius *= factor;
}
};
桥接模式很快就展现在眼前!当然,最有趣的部分在于函数draw():这是链接Circle(包含位置和大小信息)和渲染过程的桥梁。这里的桥就是渲染器,例如:
void testBridgePattern()
{
RasterRender rr;
Circle raster_cicle{rr, 5, 5, 5.5};
raster_cicle.draw();
raster_cicle.resize(2);
raster_cicle.draw();
}
在这段代码中,桥就是RasterRender:我们声明了一个RasterRenderer对象,并将其引用传递给Circle。此后,对函数draw()的调用将会以此RasterRenderer引用为桥梁来对Circle进行渲染。如果需要调整圆的大小,则可以调用resize(),渲染过程仍旧可以正常运行,因为渲染器并不知道也不关心其所渲染的Circle对象。
桥接模式的概念很简单,它通常作为连接器或黏合剂,将两个“不相关”的组件连接起来。抽象(接口)的使用允许组件之间在不了解具体实现的情况下彼此交互。
也就是说,桥接模式的参与者确实需要意识到彼此的存在。具体来说。Circle类需要引用Renderer; Renderer也需要知道如何绘制圆(即Renderer类的成员函数draw_circle的命名由来)。这与中介者模式形成了对比,中介者模式允许对象在毫不知晓对方的情况下进行通信。
#include
#include
struct Person {
std::string name;
void greet();
Person();
~Person();
class PersonImpl;
PersonImpl *impl;
};
struct Person::PersonImpl {
void greet(Person *p);
};
Person::Person() : impl(new PersonImpl()) {
}
Person::~Person() {
delete impl;
}
void Person::greet() {
impl->greet(this);
}
void Person::PersonImpl::greet(Person *p) {
printf("hello %s!\n", p->name.c_str());
}
void testPimpl() {
Person p;
p.name = "Sarry";
p.greet();
}
struct Render {
virtual void render_circle(float x, float y, float radius) = 0;
};
struct VectorRender: Render
{
void render_circle(float x, float y, float radius) override {
std::cout << "\nDrawing a vector circle of radius " << radius << "\n";
}
};
struct RasterRender:Render
{
void render_circle(float x, float y, float radius) override {
std::cout << "\nRasterizing circle of radius " << radius << "\n";
}
};
struct Shape
{
protected:
Render& render;
Shape(Render& render) : render(render) {}
public:
virtual void draw() = 0;
virtual void resize(float factor) = 0;
};
struct Circle: Shape
{
float x;
float y;
float radius;
Circle(Render& render, float x, float y, float radius)
:Shape(render), x(x), y(y), radius(radius) {}
void draw() override {
render.render_circle(x, y, radius);
}
void resize(float factor) override {
radius *= factor;
}
};
void testBridgePattern()
{
RasterRender rr;
Circle raster_cicle{rr, 5, 5, 5.5};
raster_cicle.draw();
raster_cicle.resize(2);
raster_cicle.draw();
}
int main()
{
testPimpl();
testBridgePattern();
return 0;
}