C++慣用法:奇特的遞歸模板模式(Curiously Recurring Template Pattern,CRTP,Mixin-from-above)

C++慣用法:奇特的遞歸模板模式(Curiously Recurring Template Pattern,CRTP,Mixin-from-above)

分類: C++ 2166人閱讀 評論(2) 收藏 舉報

意圖:

使用派生類作爲模板參數特化基類。

 

與多態的區別:

多態是動態綁定(運行時綁定),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; }
};
       通常,你可以在子類中訪問基類中一個保護函數。現在的挑戰是如何反過來訪問。
      
發佈了12 篇原創文章 · 獲贊 228 · 訪問量 578萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章