意图:
使用派生类作为模板参数特化基类。
与多态的区别:
多态是动态绑定(运行时绑定),CRTP是静态绑定(编译时绑定)
在实现多态时,需要重写虚函数,因而这是运行时绑定的操作。
然而如果想在编译期确定通过基类来得到派生类的行为,CRTP便是一种独佳选择,它是通过派生类覆盖基类成员函数来实现静态绑定的。
范式:
示例代码:
缺点:
CRTP由于基类使用了模板,目前的编译器不支持模板类的导出,因而不能使用导出接口。
其它使用领域:
在数值计算中,往往要对不同的模型使用不同的计算方法(如矩阵),一般使用继承提供统一接口(如operator运算符),但又希望不损失效率。这时便又可取CRTP惯用法,子类的operator实现将覆盖基类的operator实现,并可以编译期静态绑定至子类的方法。
英文链接:http://en.wikibooks.org/wiki/More_C++_Idioms/Curiously_Recurring_Template_Pattern
以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)使用CRTP做更好的封装
真正的实现在派生类中。下面是一个最简单的OO框架实现代码:
// Library code
class Base
{
};
// User code
class Derived : public Base
{
};
// Library code
template<class DerivedT>
class Base
{
{
}
{
}
};
// User code
class Derived : public Base<Derived>
{
{
return 0;
}
};
// User code
class Derived : public Base<Derived>
{
};
// Library code
class Accessor
{
};
// Library code
template<class DerivedT>
class Base
{
{
}
};
// User code
class Derived : public Base<Derived>
{
};
class Derived : public Base<Derived> { };
// User code
class Derived : public Base<Derived>
{
};
struct BreakProtection : Derived
{
};
return derived.do_foo();
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::do_foo;
struct BreakProtection : Derived
{
};
// Library code
template<class DerivedT>
class Base
{
{
}
{
return accessor::foo( this->derived());
}
};
// User code
struct Derived : Base<Derived>
};
return (derived.*(&accessor::do_foo))();
同时在编译的时候,代码类型是可以被编译器导出的,所以生成的代码也会更小。(当然,上面提到的第二种方法还是有了类型信息。我们希望未来的主流编译器都可以将其优化。)还有就是,使用成员函数指针不是很便利,尤其是重载函数。
[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++.
译者评注:C++中的静多态和CRTP(奇异递归模板模式)已经不像5年前那样令人陌生。记得当时在国内论坛上讨论静多态的时候,很多人都是不约而同的反应出?号。本人第一次接触这些也是在《C++ Template中文版》。还记得那个时候通宵达旦的看完《C++ Template中文版》之后,兴奋的想把所有类都用模板重构。然而做技术和做产品始终不同,做技术可以走在行业的前沿,不断的创新和改造,然而做产品则需要中规中矩,基础稳定,便于维护的简单代码将永远放在首位。所以这几年来对于C++ 0X 的关注越来越少。有时看到一些同事写的代码,我总是特别强调代码的语义。极限编程中说过:“源代码就是注释”。其实这句话并不是强调你不用去写帮助文档,就是强调代码的语义。举一个例子吧:一个类的析构函数到底是否为virutal,并不是取决你当时的心情。STL中的容器类的析构函数都不会定义virtual,是因为没有这个关键字,就明确告诉你了这个类不希望被继承。这其实就是代码的语义。之所以翻译这篇文章,并不是想向大家讲述CRTP,而是翻译了作者的态度。实现静多态很简单,但是实现好的CRTP却需要注意太多细节。本文从开始提出了一个想法,到最后通过技巧来写出了一个方法,直至最后完善成几乎完美,这无处不体现着国外技术高手的对于技术一丝不苟的精神。想想为什么我们永远不可能写出来《Modern C++ Design》那样惊世骇俗的宝典?也许当自己有一天也习惯了利用随想设计,利用口语编程,同时对手下指指点点的时候,道理就会不言而喻了。