# Item 35: 考慮可選的 virtual functions(虛擬函數)的替代方法
作者:Scott Meyers
譯者:fatalerror99 (iTePub's Nirvana)
發布:http://blog.csdn.net/fatalerror99/
現在你工作在一個視頻游戲上,你在游戲中為角色設計了一個 hierarchy(繼承體系)。你的游戲中有著變化多端的惡劣環境,角色被傷害或者其它的健康狀態降低的情況并不罕見。因此你決定提供一個 member function(成員函數)healthValue,它返回一個象征角色健康狀況如何的整數。因為不同的角色計算健康值的方法可能不同,將 healthValue 聲明為 virtual(虛擬)似乎是顯而易見的設計選擇:
```
class GameCharacter {
public:
virtual int healthValue() const; // return character's health rating;
... // derived classes may redefine this
};
```
healthValue 沒有被聲明為 pure virtual(純虛)的事實暗示這里有一個計算健康值的缺省算法(參見 Item 34)。
這確實是一個顯而易見的設計選擇,而在某種意義上,這是它的缺點。因為這樣的設計過于顯而易見,你可能不會對它的其它可選方法給予足夠的關注。為了幫助你脫離 object-oriented design(面向對象設計)的習慣性道路,我們來考慮一些處理這個問題的其它方法。
The Template Method Pattern via the Non-Virtual Interface Idiom(經由非虛擬接口慣用法實現的模板方法模式)
我們以一個主張 virtual functions(虛擬函數)應該幾乎總是為 private(私有的)的有趣觀點開始。這一觀點的擁護者提出:一個較好的設計應該保留作為 public member function(公有成員函數)的 healthValue,但應將它改為 non-virtual(非虛擬的)并讓它調用一個 private virtual function(私有虛擬函數)來做真正的工作,也就是說,doHealthValue:
```
class GameCharacter {
public:
int healthValue() const // derived classes do not redefine
{ // this - see Item 36
... // do "before" stuff - see below
int retVal = doHealthValue(); // do the real work
... // do "after" stuff - see below
return retVal;
}
...
private:
virtual int doHealthValue() const // derived classes may redefine this
{
... // default algorithm for calculating
} // character's health
};
```
在這個代碼(以及本 Item 的其它代碼)中,我在類定義中展示 member functions(成員函數)的本體。就像 Item 30 中所解釋的,這會將它們隱式聲明為 inline(內聯)。我用這種方法展示代碼僅僅是這樣更易于看到它在做些什么。我所描述的設計與是否 inline 化無關,所以不必深究 member functions(成員函數)定義在類的內部有什么意味深長的含義。根本沒有。
這個基本的設計——讓客戶通過 public non-virtual member functions(公有非虛擬成員函數)調用 private virtual functions(私有虛擬函數)——被稱為 non-virtual interface (NVI) idiom(非虛擬接口慣用法)。這是一個更通用的被稱為 Template Method(一個模式,很不幸,與 C++ templates(模板)無關)的 design pattern(設計模式)的特殊形式。我將那個 non-virtual function(非虛擬函數)(例如,healthValue)稱為 virtual function's wrapper(虛擬函數的外殼)。
NVI idiom(慣用法)的一個優勢通過 "do 'before' stuff" 和 "do 'after' stuff" 兩個注釋在代碼中標示出來。這些注釋標出的代碼片斷在做真正的工作的 virtual function(虛擬函數)之前或之后調用。這就意味著那個 wrapper(外殼)可以確保在 virtual function(虛擬函數)被調用前,特定的背景環境被設置,而在調用結束之后,這些背景環境被清理。例如,"before" stuff 可以包括鎖閉一個 mutex(互斥體),生成一條日志條目,校驗類變量和函數的 preconditions(前提條件)是否被滿足,等等。"after" stuff 可以包括解鎖一個 mutex(互斥體),校驗函數的 postconditions(結束條件),類不變量的恢復,等等。如果你讓客戶直接調用 virtual functions(虛擬函數),確實沒有好的方法能夠做到這些。
涉及 derived classes(派生類)重定義 private virtual functions(私有虛擬函數)(這些重定義函數它們不能調用!)的 NVI idiom 可能會攪亂你的頭腦。這里沒有設計上的矛盾。重定義一個 virtual function(虛擬函數)指定如何做某些事。調用一個 virtual function(虛擬函數)指定什么時候去做。互相之間沒有關系。NVI idiom 允許 derived classes(派生類)重定義一個 virtual function(虛擬函數),這樣就給了它們控制功能如何實現的能力,但是 base class(基類)保留了決定函數何時被調用的權利。乍一看很奇怪,但是 C++ 規定 derived classes(派生類)可以重定義 private inherited virtual functions(私有的通過繼承得到的函數)是非常明智的。
在 NVI idiom 之下,virtual functions(虛擬函數)成為 private(私有的)并不是絕對必需的。在一些 class hierarchies(類繼承體系)中,一個 virtual function(虛擬函數)的 derived class(派生類)實現被期望調用其 base class(基類)的對應物(例如,第 120 頁的例子),而為了這樣的調用能夠合法,虛擬必須成為 protected(保護的),而非 private(私有的)。有時一個 virtual function(虛擬函數)甚至必須是 public(公有的)(例如,polymorphic base classes(多態基類)中的 destructors(析構函數)——參見 Item 7),但這樣一來 NVI idiom 就不能被真正應用。
The Strategy Pattern via Function Pointers(經由函數指針實現的策略模式)
NVI idiom 是 public virtual functions(公有虛擬函數)的有趣的可選替代物,但從設計的觀點來看,它比裝點門也多不了多少東西。畢竟,我們還是在用 virtual functions(虛擬函數)來計算每一個角色的健康值。一個更引人注目的設計主張認為計算一個角色的健康值不依賴于角色的類型——這樣的計算根本不需要成為角色的一部分。例如,我們可能需要為每一個角色的 constructor(構造函數)傳遞一個指向健康值計算函數的指針,而我們可以調用這個函數進行實際的計算:
```
class GameCharacter; // forward declaration
// function for the default health calculation algorithm
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
typedef int (*HealthCalcFunc)(const GameCharacter&);
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthFunc(hcf)
{}
int healthValue() const
{ return healthFunc(*this); }
...
private:
HealthCalcFunc healthFunc;
};
```
這個方法是另一個通用 design pattern(設計模式)—— Strategy 的簡單應用,相對于基于 GameCharacter hierarchy(繼承體系)中的 virtual functions(虛擬函數)的方法,它提供了某些更引人注目的機動性:
* 相同角色類型的不同實例可以有不同的健康值計算函數。例如:
```
class EvilBadGuy: public GameCharacter {
public:
explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc)
: GameCharacter(hcf)
{ ... }
...
};
int loseHealthQuickly(const GameCharacter&); // health calculation
int loseHealthSlowly(const GameCharacter&); // funcs with different
// behavior
EvilBadGuy ebg1(loseHealthQuickly); // same-type charac-
EvilBadGuy ebg2(loseHealthSlowly); // ters with different
// health-related
// behavior
```
* 對于一個指定的角色健康值的計算函數可以在運行時改變。例如,GameCharacter 可以提供一個 member function(成員函數)setHealthCalculator,它被允許代替當前的健康值計算函數。
在另一方面,健康值計算函數不再是 GameCharacter hierarchy(繼承體系)的一個 member function(成員函數)的事實,意味著它不再擁有訪問它所計算的那個對象內部構件的特權。例如,defaultHealthCalc 不能訪問 EvilBadGuy 的 non-public(非公有)構件。如果一個角色的健康值計算能夠完全基于通過角色的 public interface(公有接口)可以得到的信息,這就沒什么問題,但是,如果準確的健康值計算需要 non-public(非公有)信息,就會有問題。實際上,在任何一個你要用 class(類)外部的等價機能(例如,經由一個 non-member non-friend function(非成員非友元函數)或經由另一個 class(類)的 non-friend member function(非友元成員函數))代替 class(類)內部的機能(例如,經由一個 member function(成員函數))的時候,它都是一個潛在的問題。這個問題將持續影響本 Item 的剩余部分,因為所有我們要考慮的其它設計選擇都包括 GameCharacter hierarchy(繼承體系)的外部函數的使用。
作為一個通用規則,解決對“non-member functions(非成員函數)對類的 non-public(非公有)構件的訪問的需要”的唯一方法就是削弱類的 encapsulation(封裝性)。例如,class(類)可以將 non-member functions(非成員函數)聲明為 friends(友元),或者,它可以提供對“在其它情況下它更希望保持隱藏的本身的實現部分”的 public accessor functions(公有訪問者函數)。使用一個 function pointer(函數指針)代替一個 virtual function(虛擬函數)的優勢(例如,具有逐對象健康值計算函數的能力和在運行時改變這樣的函數的能力)是否能抵消可能的降低 GameCharacter 的 encapsulation(封裝性)的需要是你必須在設計時就做出決定的重要部分。
The Strategy Pattern via tr1::function(經由 tr1::function 實現的策略模式)
一旦你習慣了 templates(模板)和 implicit interfaces(隱式接口)(參見 Item 41)的應用,function-pointer-based(基于函數指針)的方法看上去就有些死板了。健康值的計算為什么必須是一個 function(函數),而不能是某種簡單的行為類似 function(函數)的東西(例如,一個 function object(函數對象))?如果它必須是一個 function(函數),為什么不能是一個 member function(成員函數)?為什么它必須返回一個 int,而不是某種能夠轉型為 int 的類型?
如果我們用一個 tr1::function 類型的對象代替一個 function pointer(函數指針)(諸如 healthFunc),這些約束就會消失。就像 Item 54 中的解釋,這樣的對象可以持有 any callable entity(任何可調用實體)(例如,function pointer(函數指針),function object(函數對象),或 member function pointer(成員函數指針)),這些實體的標志性特征就是兼容于它所期待的東西。我們馬上就會看到這樣的設計,這次使用了 tr1::function:
```
class GameCharacter; // as before
int defaultHealthCalc(const GameCharacter& gc); // as before
class GameCharacter {
public:
// HealthCalcFunc is any callable entity that can be called with
// anything compatible with a GameCharacter and that returns anything
// compatible with an int; see below for details
typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthFunc(hcf)
{}
int healthValue() const
{ return healthFunc(*this); }
...
private:
HealthCalcFunc healthFunc;
};
```
就像你看到的,HealthCalcFunc 是一個 tr1::function instantiation(實例化)的 typedef。這意味著它的行為類似一個普通的 function pointer(函數指針)類型。我們近距離看看 HealthCalcFunc 究竟是一個什么東西的 typedef:
```
std::tr1::function<int (const GameCharacter&)>
```
這里我突出了這個 tr1::function instantiation(實例化)的“target signature(目標識別特征)”。這個 target signature(目標識別特征)是“取得一個引向 const GameCharacter 的 reference(引用),并返回一個 int 的函數”。這個 tr1::function 類型的(例如,HealthCalcFunc 類型的)對象可以持有兼容于這個 target signature(目標識別特征)的 any callable entity(任何可調用實體)。兼容意味著這個實體的參數能夠隱式地轉型為一個 const GameCharacter&,而它的返回類型能夠隱式地轉型為一個 int。
與我們看到的最近一個設計(在那里 GameCharacter 持有一個指向一個函數的指針)相比,這個設計幾乎相同。僅有的區別是目前的 GameCharacter 持有一個 tr1::function 對象——指向一個函數的 generalized(泛型化)指針。除了達到“clients(客戶)在指定健康值計算函數時有更大的靈活性”的效果之外,這個變化是如此之小,以至于我寧愿對它視而不見:
```
short calcHealth(const GameCharacter&); // health calculation
// function; note
// non-int return type
struct HealthCalculator { // class for health
int operator()(const GameCharacter&) const // calculation function
{ ... } // objects
};
class GameLevel {
public:
float health(const GameCharacter&) const; // health calculation
... // mem function; note
}; // non-int return type
class EvilBadGuy: public GameCharacter { // as before
...
};
class EyeCandyCharacter: public GameCharacter { // another character
... // type; assume same
}; // constructor as
// EvilBadGuy
EvilBadGuy ebg1(calcHealth); // character using a
// health calculation
// function
EyeCandyCharacter ecc1(HealthCalculator()); // character using a
// health calculation
// function object
GameLevel currentLevel;
...
EvilBadGuy ebg2( // character using a
std::tr1::bind(&GameLevel::health, // health calculation
currentLevel, // member function;
_1) // see below for details
);
```
就個人感覺而言:我發現 tr1::function 能讓你做的事情是如此讓人驚喜,它令我渾身興奮異常。如果你沒有感到興奮,那可能是因為你正目不轉睛地盯著 ebg2 的定義并對 tr1::bind 的調用會發生什么迷惑不解。請耐心地聽我解釋。
比方說我們要計算 ebg2 的健康等級,應該使用 GameLevel class(類)中的 health member function(成員函數)。現在,GameLevel::health 是一個被聲明為取得一個參數(一個引向 GameCharacter 的引用)的函數,但是它實際上取得了兩個參數,因為它同時得到一個隱式的 GameLevel 參數——指向 this。然而,GameCharacters 的健康值計算函數只取得單一的參數:將被計算健康值的 GameCharacter。如果我們要使用 GameLevel::health 計算 ebg2 的健康值,我們必須以某種方式“改造”它,以使它適應只取得唯一的參數(一個 GameCharacter),而不是兩個(一個 GameCharacter 和一個 GameLevel)。在本例中,我們總是要使用 currentLevel 作為 GameLevel 對象來計算 ebg2 的健康值,所以每次調用 GameLevel::health 計算 ebg2 的健康值時,我們就要 "bind"(凝固)currentLevel 來作為 GameLevel 的對象來使用。這就是 tr1::bind 的調用所做的事情:它指定 ebg2 的健康值計算函數應該總是使用 currentLevel 作為 GameLevel 對象。
我們跳過一大堆的細節,諸如為什么 "_1" 意味著“當為了 ebg2 調用 GameLevel::health 時使用 currentLevel 作為 GameLevel 對象”。這樣的細節并沒有什么啟發性,而且它們將轉移我所關注的基本點:在計算一個角色的健康值時,通過使用 tr1::function 代替一個 function pointer(函數指針),我們將允許客戶使用 any compatible callable entity(任何兼容的可調用實體)。很酷是不是?
The "Classic" Strategy Pattern(“經典的”策略模式)
如果你比 C++ 更加深入地進入 design patterns(設計模式),一個 Strategy 的更加習以為常的做法是將 health-calculation function(健康值計算函數)做成一個獨立的 health-calculation hierarchy(健康值計算繼承體系)的 virtual member function(虛擬成員函數)。做成的 hierarchy(繼承體系)設計看起來就像這樣:

如果你不熟悉 UML 記法,這不過是在表示當把 EvilBadGuy 和 EyeCandyCharacter 作為 derived classes(派生類)時,GameCharacter 是這個 inheritance hierarchy(繼承體系)的根;HealthCalcFunc 是另一個帶有 derived classes(派生類)SlowHealthLoser 和 FastHealthLoser 的 inheritance hierarchy(繼承體系)的根;而每一個 GameCharacter 類型的對象包含一個指向“從 HealthCalcFunc 派生的對象”的指針。
這就是相應的框架代碼:
```
class GameCharacter; // forward declaration
class HealthCalcFunc {
public:
...
virtual int calc(const GameCharacter& gc) const
{ ... }
...
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter {
public:
explicit GameCharacter(HealthCalcFunc *phcf = &defaultHealthCalc)
: pHealthCalc(phcf)
{}
int healthValue() const
{ return pHealthCalc->calc(*this);}
...
private:
HealthCalcFunc *pHealthCalc;
};
```
這個方法的吸引力在于對于熟悉“標準的”Strategy pattern(策略模式)實現的人可以很快地識別出來,再加上它提供了通過在 HealthCalcFunc hierarchy(繼承體系)中增加一個 derived class(派生類)而微調已存在的健康值計算算法的可能性。
Summary(概要)
這個 Item 的基本建議是當你為嘗試解決的問題尋求一個設計時,你應該考慮可選的 virtual functions(虛擬函數)的替代方法。以下是對我們考察過的可選方法的一個簡略的回顧:
* 使用 non-virtual interface idiom (NVI idiom)(非虛擬接口慣用法),這是用 public non-virtual member functions(公有非虛擬成員函數)包裝可訪問權限較小的 virtual functions(虛擬函數)的 Template Method design pattern(模板方法模式)的一種形式。
* 用 function pointer data members(函數指針數據成員)代替 virtual functions(虛擬函數),一種 Strategy design pattern(策略模式)的顯而易見的形式。
* 用 tr1::function data members(數據成員)代替 virtual functions(虛擬函數),這樣就允許使用兼容于你所需要的東西的 any callable entity(任何可調用實體)。這也是 Strategy design pattern(策略模式)的一種形式。
* 用 virtual functions in another hierarchy(另外一個繼承體系中的虛擬函數)代替 virtual functions in one hierarchy(單獨一個繼承體系中的虛擬函數)。這是 Strategy design pattern(策略模式)的習以為常的實現。
這不是一個可選的 virtual functions(虛擬函數)的替代設計的詳盡無遺的列表,但是它足以使你確信這些是可選的方法。此外,它們之間互為比較的優劣應該使你考慮它們時更為明確。
為了避免陷入 object-oriented design(面向對象設計)的習慣性道路,時不時地給車輪一些有益的顛簸。有很多其它的道路。值得花一些時間去考慮它們。
Things to Remember
可選的 virtual functions(虛擬函數)的替代方法包括 NVI 慣用法和 Strategy design pattern(策略模式)的各種變化形式。NVI 慣用法本身是 Template Method design pattern(模板方法模式)的一個實例。
將一個機能從一個 member function(成員函數)中移到 class(類)之外的某個函數中的一個危害是 non-member function(非成員函數)沒有訪問類的 non-public members(非公有成員)的途徑。
tr1::function 對象的行為類似 generalized function pointers(泛型化的函數指針)。這樣的對象支持所有兼容于一個給定的目標特征的 callable entities(可調用實體)。
- Preface(前言)
- Introduction(導言)
- Terminology(術語)
- Item 1: 將 C++ 視為 federation of languages(語言聯合體)
- Item 2: 用 consts, enums 和 inlines 取代 #defines
- Item 3: 只要可能就用 const
- Item 4: 確保 objects(對象)在使用前被初始化
- Item 5: 了解 C++ 為你偷偷地加上和調用了什么函數
- Item 6: 如果你不想使用 compiler-generated functions(編譯器生成函數),就明確拒絕
- Item 7: 在 polymorphic base classes(多態基類)中將 destructors(析構函數)聲明為 virtual(虛擬)
- Item 8: 防止因為 exceptions(異常)而離開 destructors(析構函數)
- Item 9: 絕不要在 construction(構造)或 destruction(析構)期間調用 virtual functions(虛擬函數)
- Item 10: 讓 assignment operators(賦值運算符)返回一個 reference to *this(引向 *this 的引用)
- Item 11: 在 operator= 中處理 assignment to self(自賦值)
- Item 12: 拷貝一個對象的所有組成部分
- Item 13: 使用對象管理資源
- Item 14: 謹慎考慮資源管理類的拷貝行為
- Item 15: 在資源管理類中準備訪問裸資源(raw resources)
- Item 16: 使用相同形式的 new 和 delete
- Item 17: 在一個獨立的語句中將 new 出來的對象存入智能指針
- Item 18: 使接口易于正確使用,而難以錯誤使用
- Item 19: 視類設計為類型設計
- Item 20: 用 pass-by-reference-to-const(傳引用給 const)取代 pass-by-value(傳值)
- Item 21: 當你必須返回一個對象時不要試圖返回一個引用
- Item 22: 將數據成員聲明為 private
- Item 23: 用非成員非友元函數取代成員函數
- Item 24: 當類型轉換應該用于所有參數時,聲明為非成員函數
- Item 25: 考慮支持不拋異常的 swap
- Item 26: 只要有可能就推遲變量定義
- Item 27: 將強制轉型減到最少
- Item 28: 避免返回對象內部構件的“句柄”
- Item 29: 爭取異常安全(exception-safe)的代碼
- Item 30: 理解 inline 化的介入和排除
- Item 31: 最小化文件之間的編譯依賴
- Item 32: 確保 public inheritance 模擬 "is-a"
- Item 33: 避免覆蓋(hiding)“通過繼承得到的名字”
- Item 34: 區分 inheritance of interface(接口繼承)和 inheritance of implementation(實現繼承)
- Item 35: 考慮可選的 virtual functions(虛擬函數)的替代方法
- Item 36: 絕不要重定義一個 inherited non-virtual function(通過繼承得到的非虛擬函數)
- Item 37: 絕不要重定義一個函數的 inherited default parameter value(通過繼承得到的缺省參數值)
- Item 38: 通過 composition(復合)模擬 "has-a"(有一個)或 "is-implemented-in-terms-of"(是根據……實現的)
- Item 39: 謹慎使用 private inheritance(私有繼承)
- Item 40: 謹慎使用 multiple inheritance(多繼承)
- Item 41: 理解 implicit interfaces(隱式接口)和 compile-time polymorphism(編譯期多態)
- Item 42: 理解 typename 的兩個含義
- Item 43: 了解如何訪問 templatized base classes(模板化基類)中的名字
- Item 44: 從 templates(模板)中分離出 parameter-independent(參數無關)的代碼
- Item 45: 用 member function templates(成員函數模板) 接受 "all compatible types"(“所有兼容類型”)
- Item 46: 需要 type conversions(類型轉換)時在 templates(模板)內定義 non-member functions(非成員函數)
- Item 47: 為類型信息使用 traits classes(特征類)
- Item 48: 感受 template metaprogramming(模板元編程)
- Item 49: 了解 new-handler 的行為
- Item 50: 領會何時替換 new 和 delete 才有意義
- Item 51: 編寫 new 和 delete 時要遵守慣例
- Item 52: 如果編寫了 placement new,就要編寫 placement delete
- 附錄 A. 超越 Effective C++
- 附錄 B. 第二和第三版之間的 Item 映射