您的位置:首页 > 编程语言 > C语言/C++

C++惯用法:奇特的递归模板模式(Curiously Recurring Template Pattern,CRTP,Mixin-from-above)

2014-07-03 19:40 701 查看


C++惯用法:奇特的递归模板模式(Curiously Recurring Template Pattern,CRTP,Mixin-from-above)

分类: C++2011-06-06
10:04 2166人阅读 评论(2) 收藏 举报

c++attributesstructinterfaceclass编译器

意图:

使用派生类作为模板参数特化基类。

与多态的区别:

多态是动态绑定(运行时绑定),CRTP是静态绑定(编译时绑定)

在实现多态时,需要重写虚函数,因而这是运行时绑定的操作。

然而如果想在编译期确定通过基类来得到派生类的行为,CRTP便是一种独佳选择,它是通过派生类覆盖基类成员函数来实现静态绑定的。

范式:

 示例代码:


缺点:

CRTP由于基类使用了模板,目前的编译器不支持模板类的导出,因而不能使用导出接口。

其它使用领域:

在数值计算中,往往要对不同的模型使用不同的计算方法(如矩阵),一般使用继承提供统一接口(如operator运算符),但又希望不损失效率。这时便又可取CRTP惯用法,子类的operator实现将覆盖基类的operator实现,并可以编译期静态绑定至子类的方法。

英文链接:http://en.wikibooks.org/wiki/More_C++_Idioms/Curiously_Recurring_Template_Pattern


以C++编译时多态改进性能

译者: 鬼重 原作者:Ben Sunshine-Hill

发表时间:2012-05-18浏览量:1687评论数:0挑错数:0

作者在文章中通过实例,一步步地试探性地采用了C++的运行时多态机制,编译时多态机制,以及对两者进行比较,来达到最终目的。这是篇老文章,又很长,想吃快餐的就别浪费时间了。原文网页排版不好,所以付上原文。

Introduction

Virtual functions are one of the most interesting and useful features of classes in C++. They allow for thinking of an object in terms of what type it is (Apple) as well as what category of types it belongs with (Fruit). Further, virtual functions allow for
operating on objects in ways that respect their actual types while refering to them by category. However, the power and flexibility of virtual functions comes at a price that a good programmer must weigh against the benefits. Let's take a quick look at using
virtual functions and abstract base classes and from there examine a way in which we can improve program performance while retaining the power and flexbility of virtual functions.

介绍

虚拟函数是C++类特性中最具趣味和用途的。它可以从何种类型(苹果),以及这些类型的归类又是什么(水果)的角度来看待对象。也就是说,虚拟函数可在只知对象是水果的情况下,能按对待苹果的方式去操作。然而强大灵活的虚拟函数也伴随着成本,好的编程者必会衡量这个收益的成本。我们来快速检视一下使用虚拟函数和抽象基类的情况,从这里开始,找出一种途径,以能让程序的运行得到改进,同时又保留虚拟能力和灵活性。

Along with modeling different kinds of fruit and different kinds of animals, modeling different kinds of shapes accounts for most of the polymorphism examples found in C++ textbooks. More importantly, modeling different kinds of shapes readily lends itself
to an area of game programming where improving performance is a high priority, namely graphics rendering. So modeling shapes will make a good basis for our examination.

Now, let's begin.

与建模各类水果、各种兽类相似,建模不同性质的形状,C++课本里大部分的多态教学案例,都采用过。更重要地是,建模不同性质的形状,在注重性能改进的游戏编程领域,比如图形渲染,把它作为例子更适合。现在让我们开个头。

class Shape

{

public:

Shape()

{

}

virtual ~Shape()

{

}

virtual void DrawOutline() const = 0;

virtual void DrawFill() const = 0;

};

class Rectangle : public Shape

{

public:

Rectangle()

{

}

virtual ~Rectangle()

{

}

virtual void DrawOutline() const

{

...

}

virtual void DrawFill() const

{

...

}

};

class Circle : public Shape

{

public:

Circle()

{

}

virtual ~Circle()

{

}

virtual void DrawOutline() const

{

...

}

virtual void DrawFill() const

{

...

}

};

All good so far, right? We can, for example, write...

目前还不错,对吧?我们可以这样写...例如

Shape *myShape = new Rectangle;

myShape->DrawOutline();

delete myShape;

...and trust C++'s runtime polymorphism to decide that the myShape pointer actually points to a Rectangle and that Rectangle's DrawOutline() method should be called. If we wanted it to be a circle instead, we could just change "new Rectangle" to "new Circle",
and Circle's DrawOutline() method would be called instead.

委托C++的运行时多态去判定myShape指针确实指向一个矩形Rectangle,然后Rectangle的DrawOutline()方法会被调用。假定它是一个圆形circle,我们把"new Rectangle"改为"new Circle",那么Circle的DrawOutline()方法会被调用。

But wait a second. Thanks, C++, for the runtime polymorphism, but it's pretty obvious from looking at that code that myShape is going to be a Rectangle; we don't need fancy vtables to figure that out. Consider this code:

慢着,不用麻烦C++的运行时多态机制了,看看代码,非常显然,myShape必定就是Rectangle;我们不必偏用虚表去搞定这件事。看如下代码:

void DrawAShapeOverAndOver(Shape* myShape)

{

for(int i=0; i<10000; i++)

{

myShape->DrawOutline();

}

}

Shape *myShape = new Rectangle;

DrawAShapeOverAndOver(myShape);

delete myShape;

Look at what happens there! The program picks up myShape, inspects it, and says "Hmm, a Rectangle. Ok." Then it puts it down. Then it picks it up again. "Hmm. This time, it's a Rectangle. Ok. Hmm, and this time it's a... Rectangle. Ok." Repeat 9,997 times.
Does all this type inspection eat up CPU cycles? Darn tootin' it does. Although virtual function calls aren't what you'd call slow, even a small delay really starts to add up when you're doing it 10,000 times per object per frame. The real tragedy here is
that we know that the program doesn't really need to check myShape's type each time through the loop. "It's always going to be the same thing!", we shout at the compiler, "Just have the program look it up the first time!" For that matter, it doesn't really
need to be looked up the first time. Because we are calling it on a Rectangle that we have just created, the object type is still going to be a Rectangle when DrawAShapeOverAndOver() gets to it.

发生了什么!程序执行过程中遇到myShape,检查它,说“唔,是Rectangle。好的。”然后通过。然后又遇到。“唔,这次,是Rectangle。好的。唔,这次,一个... Rectangle。好的。”重复9,997次。所有类型检查会吞噬CPU时钟周期吗?当然会。尽管虚拟函数调用并非慢到哪里去,甚至在每个对象每一帧作10,000次调用,小延迟开始累加起来的时候,也是如此。真正惨的是我们知道程序每次循环都检查myShape的类型是不必要的。“它总在作同样的事!”我们对编译器大嚷,“只要程序在开头检查一次啊!”实际上要解决这里的问题,也不必做那一次开头检查。因为我们调取的对象Rectangle是刚创建的,在DrawAShapeOverAndOver()得到的对象类型仍然会是一个Rectangle。

Let's see if we can rewrite this function in a way that doesn't require runtime lookups. We will make it specifically for Rectangles, so we can just flat-out tell the dumb compiler what it is and forego the lookup code.

来看看是否我们可以采用避免运行时检查的方法,来重写一次函数。我们使其专门针对Rectangles,那么就直接了当地告知死板的编译器,对象是什么类型,这样事先就作好类型检查。

void DrawAShapeWhichIsARectangleOverAndOver(Shape* myShape)

{

for(int i=0; i<10000; i++)

{

reinterpret_cast(myShape)->DrawOutline();

}

}

Unfortunately, this doesn't help one bit. Telling the compiler that the object is a Rectangle isn't enough. For all the compiler knows, the object could be a subclass of Rectangle. We still haven't prevented the compiler from inserting runtime lookup code.
To do that we must remove the virtual keyword from the declaration of DrawOutline() and thereby change it into a non-virtual function. That means in turn, however, that we have to declare a separate DrawAShapeOverAndOver() for each and every subclass of Shape
that we might want to draw. Alas, pursuing our desire for efficiency has driven us further and further away from our goal, to the point where there is barely any polymorphism left at all. So sad.

倒楣,这种代码于事无补。告知编译器对象是Rectangle还不够。编译器都明白,对象有可能是Rectangle的派生类。我们还是未能阻止编译器插入运行时检查代码。必须移掉DrawOutline()的virtual关键字,然后改成非虚拟的。按该思路推下去,还要在每个及任何想要去画出图形的Shape子类中,声明DrawAShapeOverAndOver()方法。哎,对效率需求的满足已经驱使我们愈加偏离最初目标了,仅有的多态性好处在这里也失去了。真丧气。

Thanks But No Thanks, C++

Reading over the last few paragraphs, the astute programmer will notice an interesting point: At no time did we actually need runtime polymorphism. It helped us write our DrawAShapeOverAndOver() function by letting us write a single function that would work
for all classes derived from Shape, but in each case the run-time lookup could have been done at compile-time.

目前指望不上C++,但最终还要靠它

读过上面几个段落的文字后,精明的程序员会注意到可加利用的一点:我们确实找不出需要运行时多态的时机嘛。这有助于我们单写一个DrawAShapeOverAndOver()方法,它为所有Shape的派生类共用,而在处理每种对象类型的时候,用编译时检查替换运行时检查。

Bearing this in mind, let's approach polymorphism again, but this time with more caution. We won't be making the DrawOutline() method virtual again, since so far that has done us no good at all. Instead, let's rewrite DrawAShapeOverAndOver() as a templated
function. This way we are not forced to write both DrawAShapeWhichIsARectangleOverAndOver() and DrawAShapeWhichIsACircleOverAndOver().

记下这个思路,让我们再次来使用多态特性,但这次要更审慎一些。我们不会再次把DrawOutline()方法写成虚拟的,因为目前为止它的效果根本不好。我们把DrawAShapeOverAndOver()改为模板函数。不是硬性写成DrawAShapeWhichIsARectangleOverAndOver()和DrawAShapeWhichIsACircleOverAndOver()。

template 〈typename ShapeType〉

void DrawAShapeOverAndOver(ShapeType* myShape)

{

for(int i=0; i<10000; i++)

{

myShape->DrawOutline();

}

}

Rectangle *myRectangle = new Rectangle;

DrawAShapeOverAndOver(myRectangle);

delete myRectangle;

Hey! Now we're getting somewhere! We can pass in any kind of Shape to DrawAShapeOverAndOver(), just like before, except this time there is no runtime checking of myShape's type! Interestingly enough, Rectangle and Circle don't even have to be derived from Shape.
They just have to be classes with a DrawOutline() function.

嘿!现在有眉目了!就像之前那样,可以传入不拘类别的Shape给DrawAShapeOverAndOver(),且不再需要运行时检查!Rectangle和Circle甚至都不必从Shape派生。只要这个类型具有函数DrawOutline()就可以了。

Making Our Lives More Difficult

Let's go back to our original example, but this time let's make more use of the other features of subclassing. After all, derived classes and base classes with no private members, nontrivial constructors, or internal calls of virtual functions are a rather
severe oversimplification of subclassing. Let's also supply an actual implementation of DrawOutline() and DrawFill(), albeit using a completely fictional Graphics object that will nevertheless allow us to illustrate how functions in derived classes may use
functions in base classes.

Now, let's pull out the big guns.

让我们的生活更有挑战一些

回头看原来的例子,这次给子类加入更多功能。毕竟,派生类和具有非私有成员,nontrivial constructors(案:专业术语nontrivial——不由编译器自动生成的,本色性质的)的基类,或只供虚拟函数内部调用的基类,其继承是颇为简陋的。我们还提供实际意义的DrawOutline()和DrawFill()实作,虽然里面使用完全虚构的图像对象Graphic,这不过是举例说明派生类的函数可以使用基类中的函数。现在让我们拿出点真家伙来吧。

class Shape

{

public:

Shape(const Point &initialLocation,

const std::string &initialOutlineColor,

const std::string &initialFillColor) :

location(initialLocation),

outlineColor(initialOutlineColor),

fillColor(initialFillColor)

{

}

virtual ~Shape()

{

}

virtual void DrawOutline() const = 0;

virtual void DrawFill() const = 0;

void SetOutlineColor(const std::string &newOutlineColor)

{

outlineColor = newOutlineColor;

}

void SetFillColor(const std::string &newFillColor)

{

fillColor = newFillColor;

}

void SetLocation(const Point & newLocation)

{

location = newLocation;

}

const std::string &GetOutlineColor() const

{

return outlineColor;

}

const std::string &GetFillColor() const

{

return fillColor;

}

const Point &GetLocation() const

{

return location;

}

void DrawFilled() const

{

DrawOutline();

DrawFill();

}

private:

std::string outlineColor;

std::string fillColor;

Point location;

};

class Rectangle : public Shape

{

public:

Rectangle(const Point &initialLocation,

const std::string &initialOutlineColor,

const std::string &initialFillColor(),

double initialHeight,

double initialWidth) :

Shape(initialLocation, initialOutlineColor,

initialFillColor),

height(initialHeight),

width(initialWidth)

{

}

virtual ~Rectangle()

{

}

virtual void DrawOutline() const

{

Graphics::SetColor(GetOutlineColor());

Graphics::GoToPoint(GetLocation());

Graphics::DrawRectangleLines(height, width);

}

virtual void DrawFill() const

{

Graphics::SetColor(GetOutlineColor());

Graphics::GoToPoint(GetLocation());

Graphics::DrawRectangleFill(height, width);

}

void SetHeight(double newHeight)

{

height = newHeight;

}

void SetWidth(double newWidth)

{

width = newWidth;

}

double GetHeight() const

{

return height;

}

double GetWidth() const

{

return width;

}

private:

double height;

double width;

};

class Circle : public Shape

{

public:

Circle(const Point &initialLocation,

const std::string &initialOutlineColor,

const std::string &initialFillColor,

double initialRadius) :

Shape(initialLocation, initialOutlineColor,

initialFillColor),

radius(initialRadius)

{

}

virtual ~Circle()

{

}

virtual void DrawOutline() const

{

Graphics::SetColor(GetOutlineColor());

Graphics::GoToPoint(GetLocation());

Graphics::DrawCircularLine(radius);

}

virtual void DrawFill() const

{

Graphics::SetColor(GetOutlineColor());

Graphics::GoToPoint(GetLocation());

Graphics::DrawCircularFill(radius);

}

void SetRadius(double newRadius)

{

radius = newRadius;

}

double GetRadius() const

{

return radius;

}

private:

double radius;

};

Whew! Let's see what we added there. First of all, Shape objects now have data members. All Shape objects have a location, and an outlineColor and a fillColor. In addition, Rectangle objects have a height and a width, and Circle objects have a radius. Each
of these members has corresponding getter and setter functions. The most important new addition is the DrawFilled() method, which draws both the outline and the fill in one step by delegating these methods to the derived class.

好了!来看看我们加了些什么。首先,Shape对象现在有了数据成员。所有Shape对象具备了方位,轮廓色outlineColor和填充色fillColor。Rectangle对象还具有高度和宽度值,Circle对象具有半径。每个成员都有相应的getter和setter函数。最重要的新部分是DrawFilled()方法,以达成轮廓和填充的步骤交给派生类代理的目的。

We Can Rebuild It; We Have The Technology

Now that we have this all set up, let's rip it apart! Let's tear it down and rebuild it into a class structure which invites compile-time polymorphism.

How shall we do this? First, let's remove the virtual keyword from the declarations of DrawOutline() and DrawFill(). As we touched on earlier, virtual functions add runtime overhead which is precisely what we are trying to avoid. For that matter, let's go one
step further and remove the declarations of those functions from the base class altogether, as they do us no good anyway. Let's leave them in as comments, though, so that it remains clear that they were omitted on purpose.

我们有手段重写代码

把上面这些代码完成后,我们来把它分拆掉,要改写类结构,令他们具备编译时多态的特点。

怎么作呢?首先,移除DrawOutline()和DrawFill()声明中的virtual关键字。此前我们已经知道,虚拟函数增加运行时的负担,很明确,这是要力图避免的。有鉴于此,进一步统统移掉这些函数在基类中的声明,他们实在没什么用处。用注释方法移除这些代码,这样能显示我们注释掉他们的意图。

Now, what have we broken? Not much, actually. If we have a Rectangle, we can get and set its height and width and colors and location, and we can draw it. Life is good. However, one thing that we have broken is the DrawFilled() function, which calls the now
nonexistent base class functions DrawOutline() and DrawFill(). Base classes can only call functions of derived classes if those functions are declared as virtual in the base class--which is precisely what we do not want.

现在,有什么东西被我们改坏了?还好,的确没那么烂。假如我们有一个Rectangle对象,我们能访问他的高、宽、色调和位置,而且还能画出它的样子。一切都很妙。不管怎样,函数DrawFilled()是需要修改的一处代码,目前它调用着基类中已不存在的函数DrawOutline()和DrawFill()。如果这些函数在基类中声明为虚拟的,基类自然可以调用派生类的函数,但这正好是我们不需要的。

In order to fix the broken DrawFilled() function, we will use templates in a very strange and interesting way. Here's a bit of code to broadly illustrate the insanity that is to come.

为了修改这个已经失效了的DrawFilled(),我们将引入一种很奇特有趣的方式来使用模板机制。看下面的代码所展示出一种即将面临的极致手法。

template 〈typename ShapeType〉

class Shape

{

...

protected:

Shape( ... )

{

}

};

class Rectangle : public Shape〈Retangle〉

{

public:

Rectangle( ... ) :

Shape( ... )

{

}

...

};

Whaaa? That's right: Rectangle no longer inherits from Shape; now it inherits from a special kind of Shape. Rectangle creates its own special Shape class, Shape〈Rectangle〉, to inherit from. In fact, Rectangle is the only class that inherits from this specially
crafted Shape〈Rectangle〉. To enforce this, we declare the constructor of the templated Shape class protected so that an object of this type can not be instanced directly. Instead, this special kind of Shape must be inherited from and instanced within the public
constructor of the derived class.

呃?就是这样:Rectangle不再继承Shape;现在它继承自一个特殊类型的Shape。Rectangle创建自己定制的Shape类型:Shape〈Rectangle〉,同时又继承于它。实际上,Rectangle是唯一从精巧的Shape〈Rectangle〉继承而来的类。为确保唯一性,其构造函数声明为保护级,以使该类型的对象不可能直接实例化。而是只在这个Shape〈Rectangle〉必须被继承的条件下,利用派生类的构造函数去实例化。

Yes, it's legal. Yes, it's strange. Yes, it's necessary. It's called the "Curiously Recurring Template Pattern" (or Idiom, depending on who you ask).

是的,这么写合法。是的,怪里怪气。是的,必需如此。这种写法叫作"Curiously Recurring Template Pattern[奇异递归模板模式]" (模式或叫做惯用法,不同的人,叫法不一)。

But why? What could this strange code possibly gain us??

What we gain is the template parameter. The base class Shape now knows that it really is the Shape part of a Rectangle because we have told it so through the template parameter, and because we have taken a solemn oath that the only class that ever inherits
Shapeis Rectangle. If Shapeever wonders what subclass it's a part of, it can just check its ShapeType template parameter.

What this knowledge gains us, in turn, is the ability to downcast. Downcasting is taking an object of a base class and casting it as an object of a derived class. It's what dynamic_cast does for virtual classes, and it's what virtual function calls do. It's
also what we tried to do way back near the beginning of this article, when we tried to use reinterpret_cast to convince our compiler that myShape was a Rectangle. Now that the functions aren't virtual anymore, however, this will work much better (in other
words, it'll work). Let's use it to rewrite DrawFilled().

为什么呢?这些古怪代码能给我们什么好处??

好初就在于模板参数。基类Shape清楚它自己其实是Rectangle的一部分,因为我们经由模板参数告知它这一点,而且我们下了严明的界定:从Shape继承的唯一类就是Rectangle。如果Shape要搞清楚继承它的子类是什么类型,检查它的模板参数ShapeType就知道了(案:这里的检查指静态类型检查)。这个参数信息带来的好处,是向下转型(downcast)的能力。downcast是指把一个基类对象转型为一个派生类对象。相当于dynamic_cast在虚拟类以及调用虚拟函数之时,所负责的事情。也就是我们在文章开始不久就尝试过的动态机制,那时,我们用了reinterpret_cast,使编译器认为myShape是个Rectangle。呐,现在,函数不再是虚拟的,这样会更好(也就是说,切合我们的目标)。我们照此重写DrawFilled()。

template〈typename ShapeType〉

class Shape

{

void DrawFilled()

{

reinterpret_cast〈const ShapeType*〉(this)->DrawOutline();

reinterpret_cast〈const ShapeType*〉(this)->DrawFill();

}

};

Take a moment to cogitate on this code. It's possibly the most crucial part of this entire article. When DrawFilled() is called on a Rectangle, even though it is a method defined in Shape and thus called with a this pointer of type Shape, it knows that it can
safely treat itself as a Rectangle. This lets Shape reinterpret_cast itself down to a Rectangle and from there call DrawOutline() on the resultant Rectangle. Ditto with DrawFill().

花点时间思考上面的代码。这可能是文章最关键的一部分。DrawFilled()处理Rectangle,虽然它只是调用Shape的方法,并且采用Shape模板类型的this指针来调用的,但是很清楚,DrawFilled()这种把this当作Rectangle对象是安全的。这样我们就能够直接把Shape指针reinterpret_cast(强制转型)为Rectangle,然后用它调用Rectangle的DrawOutline()。DrawFill()也如法炮制。

Putting It Together

So let's put it all together.

整合起来

那么让我们把它们整合在一起。

template

class Shape

{

public:

~Shape()

{

}

/* Omitted from the base class and

declared instead in subclasses */

/* void DrawOutline() const = 0; */

/* void DrawFill() const = 0; */

void SetOutlineColor(const std::string &newOutlineColor)

{

outlineColor = newOutlineColor;

}

void SetFillColor(const std::string &newFillColor)

{

fillColor = newFillColor;

}

void SetLocation(const Point &newLocation)

{

location = newLocation;

}

const std::string &GetOutlineColor() const

{

return outlineColor;

}

const std::string &GetFillColor() const

{

return fillColor;

}

const Point &GetLocation() const

{

return location;

}

void DrawFilled() const

{

reinterpret_cast(this)->DrawOutline();

reinterpret_cast(this)->DrawFill();

}

protected:

Shape(const Point &initialLocation,

const std::string &initialOutlineColor,

const std::string &initialFillColor) :

location(initialLocation),

outlineColor(initialOutlineColor),

fillColor(initialFillColor)

{

}

private:

std::string outlineColor;

std::string fillColor;

Point location;

};

class Rectangle : public Shape〈Rectangle〉

{

public:

Rectangle(const Point &initialLocation,

const std::string &initialOutlineColor,

const std::string &initialFillColor,

double initialHeight,

double initialWidth) :

Shape〈Rectangle〉(initialLocation, initialOutlineColor,

initialFillColor),

height(initialHeight),

width(initialWidth)

{

}

~Rectangle()

{

}

void DrawOutline() const

{

Graphics::SetColor(GetOutlineColor());

Graphics::GoToPoint(GetLocation());

Graphics::DrawRectangleLines(height, width);

}

void DrawFill() const

{

Graphics::SetColor(GetOutlineColor());

Graphics::GoToPoint(GetLocation());

Graphics::DrawRectangleFill(height, width);

}

void SetHeight(double newHeight)

{

height = newHeight;

}

void SetWidth(double newWidth)

{

width = newWidth;

}

double GetHeight() const

{

return height;

}

double GetWidth() const

{

return width;

}

private:

double height;

double width;

};

class Circle : public Shape〈Circle〉

{

public:

Circle(const Point &initialLocation,

const std::string &initialOutlineColor,

const std::string &initialFillColor,

double initialRadius) :

Shape(initialLocation, initialOutlineColor,

initialFillColor),

radius(initialRadius)

{

}

~Circle()

{

}

void DrawOutline() const

{

Graphics::SetColor(GetOutlineColor());

Graphics::GoToPoint(GetLocation());

Graphics::DrawCircularLine(radius);

}

void DrawFill() const

{

Graphics::SetColor(GetOutlineColor());

Graphics::GoToPoint(GetLocation());

Graphics::DrawCircularFill(radius);

}

void SetRadius(double newRadius)

{

radius = newRadius;

}

double GetRadius() const

{

return radius;

}

private:

double radius;

};

This is just what we need! Base class functions can defer certain functionality to derived classes and derived classes can decide which base class functions to override. If we had declared a non-virtual DrawOutline() function in Shape (rather than leaving it
in only as a comment), it would be optional for Circle and Rectangle to override it. This approach allows programmers using a class to not concern themselves with whether a function is in the derived class or inherited from the base class. It's the functionality
that we had in the last section, but without the added overhead of run-time polymorphism.

While we're at it, let's rewrite DrawAShapeOverAndOver().

这就满足了我们的要求。基类函数推延某些功能的实现,把他们留给派生类完成,派生类又能决定重载哪些基类函数。假如声明一个Shape的非虚拟的DrawOutline()函数(并非注释掉),他们可被Circle Rectangle重载。这个方法允许编程者在使用类的时候不必关心函数来自派生类还是继承于基类。这是前面的段落中,我们已涉及的功能,但是没有额外的运行时多态开销。

template 〈typename ShapeType〉

void DrawAShapeOverAndOver(ShapeType* myShape)

{

for(int i=0; i<10000; i++)

{

myShape->DrawOutline();

// OR

myShape->DrawFilled();

}

}

Rectangle *rectangle = new Rectangle;

DrawAShapeOverAndOver(rectangle);

delete rectangle;

Notice that we can call member functions declared either in the derived class or the base class. Of course, if the templated function uses member functions defined in only a particular derived class (such as GetRadius()), the templated function will not compile
if used with a class that does not have those member functions. For example, calling GetRadius() on a Rectangle will not compile.

注意,我们可以调用成员函数,无论它申明于派生类或基类中间。当然,如果模板函数使用了定义在特定派生类中的成员函数(如GetRadius),如果该类没有这个成员函数,模板函数不会通过编译。比如,以Rectangle类型调用GetRadius(),就不会通过编译。

Limitations

The biggest limitation of compile-time polymorphism is that it's compile-time. In other words, if we want to call a function on a Rectangle, we can't do it through a pointer to a Shape. In fact, there is no such thing as a pointer to a Shape, since there is
no Shape class without a template argument.

This is less of a limitation than you might think. Take another look at our rewritten DrawAShapeOverAndOver():

局限

编译时多态的最大局限就是编译时。换句话讲,如果调用Rectangle的函数,则不能通过Shape类型的指针达到这个目的。实际上,没有这样一种指向Shape的指针,因为不指定模板参数的Shape类是不存在的。这可能比我们想象的限制要少一些。看看另外重写的DrawAShapeOverAndOver():

template 〈typename ShapeType〉

void DrawAShapeOverAndOver(ShapeType* myShape)

{

for(int i=0; i<10000; i++)

{

myShape->DrawOutline();

}

}

Essentially, wherever you once had functions that took in base class pointers, you now have templated functions that take in derived class pointers (or derived classes). The responsibility for calling the correct member function is delegated to the outer templated
function, not to the object.

基本上,曾是以基类指针去调取函数的地方,你现在都得通过模板化的函数,来使用派生类指针(或是派生类)。调用正确成员函数的任务,委托给外部模板函数来负责,而不是对象了。

Templates have drawbacks. Although the best way to get a feel for these drawbacks is to experience them yourself, it's also a good idea for a programmer to have an idea of what to expect. First and foremost is that most compilers require templates to be declared
inline. This means that all your templated functions will have to go in the header, which can make your code less tidy. (If you're using the Comeau compiler, this doesn't apply to you. Congratulations.)

模板有弊端。尽管最好是自己去体验一番,但是编程者对于事情有个预估,未必不是个好主意。首先一点是,多数编译器要求模板declared inline。意思是所有模板要放在头文件里,这导致代码不太整洁有序。(如果你在用Comeau compiler,不用担心这个,恭喜你。)

Secondly, templates can lead to code bloat, since different versions of the functions must be compiled for each datatype they are used with. How much code bloat is caused is very specific to the project; switching all of my content loading functions to use
this model increased my stripped executable size by about 50k. As always, the best source of wisdom is your own tests.

第二点,导致代码膨胀,因为每个用到的具体datatype类型,相应函数版本必须编译。有多少代码膨胀取决于具体的项目;去除无关部分,并转为目前这种编码设计后,净执行部分的大小,增加了约50k。你自己去测试是最明智的。

Summary

Using templates for compile-time polymorphism can increase performance when they are used to avoid needless virtual function binding. With careful design, templates can be used to give non-virtual classes all the capabilities that virtual classes have, except
for runtime binding. Although such compile-time polymorphism is not appropriate for every situation, a careful decision by the programmer as to where virtual functions are actually needed can dramatically improve code performance, without incurring a loss
of flexibility or readability.

结语

编译时多态能改善性能,避免无谓的虚拟函数绑定。经过仔细设计,模板能够给非虚拟类带来虚拟类的全部功能,除了运行时绑定。编译时多态虽不是任何情形下都适用,但是,在那些的确需要应用虚拟功能大幅度改进性能,又不至于损失代码灵活性或可读性的地方,编译时多态就成为程序员的一项严肃选择了。


c++ Template CRTP

(2009-12-22 13:37:57)


转载▼

标签:


杂谈

分类: Cplusplus
Better Encapsulation for the Curiously Recurring Template Pattern

使用CRTP做更好的封装

长久以来,C++一直突出于优秀的技巧和典范。老有名气的一个就是James Coplien在1995年提出的奇异递归模板模式(CRTP)。自那以后,CRTP便开始流行并在多个库中使用,尤其是Boost。例如,你可以在Boost.Iterator,Boost.Python或者Boost.Serialization库中看到他们。

在这篇文章中,我假设读者已经熟悉了CRTP。如果你想温习一下的话,我推荐你去阅读《C++模板编程》的第17章。在www.informit.com上,你可以找到该章节的免费版本。

如果你抱着OO的观点去看CRTP的话,你会发现,他和OO框架的有着共同的特点,都是基类调用虚函数,

真正的实现在派生类中。下面是一个最简单的OO框架实现代码:

// Library code

class Base

{

public:

virtual ~Base();

int foo() { return this->do_foo(); }

protected:

virtual int do_foo() = 0;

};

这里,Base::foo调用了一个虚函数do_foo,他是声明在Base类中的一个纯虚函数,而且他必须在基类中实现。也就是说,do_foo的实体出现在Derived类中。

// User code

class Derived : public Base

{

private:

virtual int do_foo() { return 0; }

};

这里有个有意思的地方是do_foo函数必须将访问符从保护修改成私有。这在C++中是比较好的访问控制,同时实现它只需要键入几个简单的字符。为什么要在这里有意强调do_foo不是共有使用呢?理由是一个用户应该尽力隐藏类的实现细节从而使类更加简单。(用户如果觉得这个类没有对外暴露的价值,甚至应该隐藏整个Derived类)。

现在让我们假设,有一些限制性的因素导致virtual函数不能胜任,同时框架的作者决定使用CRTP。

// Library code

template<class DerivedT>

class Base

{

public:

DerivedT& derived()

{

return static_cast<DerivedT&>(*this);

}

int foo()

{

return this->derived().do_foo();

}

};

// User code

class Derived : public Base<Derived>

{

public:

int do_foo()

{

return 0;

}

};

尽管do_foo是同一个实现,但是它可以被任意访问。为什么不将它设置为私有或者保护?答案是在foo函数中调用了Derived::do_foo,或者说,基类直接调用了一个在派生类中的函数。

现在让我们找一个最简单方法,对于Derived的用户隐藏其实现细节。他应该足够简单,否则,用户将不会使用它。对于Base类的作者,这个稍微有些麻烦,但也应该是不难解决的。

最显而易见的方法是在Base类和Derived类之间建立一个友谊关系。

// User code

class Derived : public Base<Derived>

{

private:

friend class Base<Derived>;

int do_foo() { return 0; }

};

这个解决方案并不是很完美,只因为一个简单的理由:每一个Base的模板参数类,都要定义一个friend声明。如果模板参数较多,那么这个声明列表将会很长。

为了解决这个问题,同时将友元列表的长度固定,我们引入一个非模板类Accessor来做一次前向调用。

// Library code

class Accessor

{

private:

template<class> friend class Base;

template<class DerivedT>

static int foo(DerivedT& derived)

{

return derived.do_foo();

}

};

函数Base::foo应该称为Accessor::foo,他用来转发调用至Derived::do_foo。

首先是这个调用链永远会成功,因为Base类是Accessor类的友元。

// Library code

template<class DerivedT>

class Base

{

public:

DerivedT& derived() {

return static_cast<DerivedT&>(*this); }

int foo()

{

return Accessor::foo(this->derived());

}

};

其次是当do_foo为公有或者当do_foo是保护同时Accessor类是Derived类的一个友元时才会成功。我们只感兴趣第二种情况。

// User code

class Derived : public Base<Derived>

{

private:

friend class Accessor;

int do_foo() { return 0; }

};

这种方法被boost的多个库使用,譬如:Boost.Python中的def_visitor_access和Boost.Iterator的iterator_core_access都应该被声明为友元,以此来访问用户从def_visitor或者iterator_facade定义的私有函数。

尽管这个解决方案很简单。但是我们还是会有一种方法可以省略友元声明这个列表。在这种情况下,do_foo不能是私有,你必须要把它修改成保护。这其实没什么,因为这两者之间的访问控制差别对于CRTP的用户来说不重要。为什么呢?让我们看一下用户将如何派生于CRTP基类。

class Derived : public Base<Derived> { };

这里,将把最终类给模板参数列表。任何试图派生于Derived的类都没有太大意义,因为基类Base<Derived>仅仅知道Derived类,不能够定义生成Derived类的对象。

由于我们不用考虑派生问题了,那么我们现在的目标就是如何实现在Base类中访问声明为protected的函数Derived::do_foo。

// User code

class Derived : public Base<Derived>

{

protected:

// No friend declaration here!

int do_foo() { return 0; }

};

通常,你可以在子类中访问基类中一个保护函数。现在的挑战是如何反过来访问。

第一步是显而易见的。因为我们唯一的切入点是一个保护函数可以被Derived的后代访问。

struct BreakProtection : Derived

{

static int foo(Derived& derived)

{

}

};

当我们试图去完成他的实体:

return derived.do_foo();

然而,BreakProtection::foo将会失败,因为根据C++标准,这个是被禁止的。

paragraph 11.5:

When a friend or a member function of a derived class references a protected nonstatic

member of a base class, an access check applies in addition to those described earlier

in clause 11. Except when forming a pointer to member (5.3.1), the access must be through

a pointer to, reference to, or object of the derived class itself

(or any class derived from that class) (5.2.5).

所以这个函数仅能被类型为BreakProtection的物体访问。

好吧,如果一个函数不能直接访问,那我们就间接访问。在BreakProtection类中得到do_foo的地址。

&BreakProtection::do_foo;

BreakProtection中并没有do_foo函数,因此,表达式将会被解析成&Derived:do_foo。

既然公有访问一个指向保护成员函数的指针是允许的,那我们就赶紧调用吧。

struct BreakProtection : Derived

{

static int foo(Derived& derived)

{

int (Derived::*fn)() = &BreakProtection::do_foo;

return (derived.*fn)();

}

};

为了更好的封装,BreakProtection可以被移动到Base模板类的私有节中。最终的解决方案是:

// Library code

template<class DerivedT>

class Base

{

private:

struct accessor : DerivedT

{

static int foo(DerivedT& derived)

{

int (DerivedT::*fn)() = &accessor::do_foo;

return (derived.*fn)();

}

};

public:

DerivedT& derived()

{

return static_cast<DerivedT&>(*this);

}

int foo()

{

return accessor::foo( this->derived());

}

};

// User code

struct Derived : Base<Derived>

protected:

int do_foo() { return 1; }

};

这回用户的代码比第一种解决方案苗条和清晰了,而库代码则没有太大的变动。

尽管如此,但还是有一个瑕疵的地方,大多数编译器不能优化间接的函数指针,即使他就是指向在原本对象的地方。

return (derived.*(&accessor::do_foo))();

无论如何,CRTP的也会比虚函数优化的更好。因为CRTP没有虚函数调用的负担,所以他的效率是很快的。

同时在编译的时候,代码类型是可以被编译器导出的,所以生成的代码也会更小。(当然,上面提到的第二种方法还是有了类型信息。我们希望未来的主流编译器都可以将其优化。)还有就是,使用成员函数指针不是很便利,尤其是重载函数。

参考文档

[Coplien] James O. Coplien. "Curiously Recurring Template Patterns", C++ Report, February 1995.

[Vandevoorde-] David Vandevoorde, Nicolai M. Josuttis. "C++ Templates: The Complete Guide". http://www.informit.com/articles/article.asp?p=31473
[Boost] Boost libraries. http://www.boost.org.
[standard] ISO-IEC 14882:1998(E),Programming languages - C++.

原文链接:http://accu.org/index.php/journals/296

译者评注:C++中的静多态和CRTP(奇异递归模板模式)已经不像5年前那样令人陌生。记得当时在国内论坛上讨论静多态的时候,很多人都是不约而同的反应出?号。本人第一次接触这些也是在《C++ Template中文版》。还记得那个时候通宵达旦的看完《C++ Template中文版》之后,兴奋的想把所有类都用模板重构。然而做技术和做产品始终不同,做技术可以走在行业的前沿,不断的创新和改造,然而做产品则需要中规中矩,基础稳定,便于维护的简单代码将永远放在首位。所以这几年来对于C++ 0X 的关注越来越少。有时看到一些同事写的代码,我总是特别强调代码的语义。极限编程中说过:“源代码就是注释”。其实这句话并不是强调你不用去写帮助文档,就是强调代码的语义。举一个例子吧:一个类的析构函数到底是否为virutal,并不是取决你当时的心情。STL中的容器类的析构函数都不会定义virtual,是因为没有这个关键字,就明确告诉你了这个类不希望被继承。这其实就是代码的语义。之所以翻译这篇文章,并不是想向大家讲述CRTP,而是翻译了作者的态度。实现静多态很简单,但是实现好的CRTP却需要注意太多细节。本文从开始提出了一个想法,到最后通过技巧来写出了一个方法,直至最后完善成几乎完美,这无处不体现着国外技术高手的对于技术一丝不苟的精神。想想为什么我们永远不可能写出来《Modern
C++ Design》那样惊世骇俗的宝典?也许当自己有一天也习惯了利用随想设计,利用口语编程,同时对手下指指点点的时候,道理就会不言而喻了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: