# Item 9: 絕不要在 construction(構造)或 destruction(析構)期間調用 virtual functions(虛擬函數)
作者:Scott Meyers
譯者:fatalerror99 (iTePub's Nirvana)
發布:http://blog.csdn.net/fatalerror99/
我以這個概述開始:你不應該在 construction(構造)或 destruction(析構)期間調用 virtual functions(虛擬函數),因為這樣的調用不會如你想象那樣工作,而且它們做的事情保證會讓你很郁悶。如果你轉為 Java 或 C# 程序員,也請你密切關注本 Item,因為在 C++ 急轉彎的地方,那些語言也緊急轉了一個彎。
假設你有一套模擬股票交易的 class hierarchy(類繼承體系),例如,購入訂單,出售訂單等。對于這樣的交易來說可供審查是非常重要的,所每次一個交易對象被創建,在一個審查日志中就需要創建一個相應的條目。下面是一個看起來似乎合理的解決問題的方法:
```
class Transaction { // base class for all
public: // transactions
Transaction();
virtual void logTransaction() const = 0; // make type-dependent
// log entry
...
};
Transaction::Transaction() // implementation of
{ // base class ctor
...
logTransaction(); // as final action, log this
} // transaction
class BuyTransaction: public Transaction { // derived class
public:
virtual void logTransaction() const; // how to log trans-
// actions of this type
...
};
class SellTransaction: public Transaction { // derived class
public:
virtual void logTransaction() const; // how to log trans-
// actions of this type
...
};
```
考慮執行這行代碼時會發生什么:
```
BuyTransaction b;
```
很明顯一個 BuyTransaction 的 constructor(構造函數)會被調用,但是首先,一個 Transaction 的 constructor(構造函數)必須先被調用,derived class objects(派生類對象)中的 base class parts(基類構件)先于 derived class parts(派生類構件)被構造。Transaction 的 constructor(構造函數)的最后一行調用 virtual functions(虛擬函數) logTransaction,但是結果會讓你大吃一驚,被調用的 logTransaction 版本是在 Transaction 中的那一個,而不是 BuyTransaction 中的那一個——即使被創建的 object (對象)類型是 BuyTransaction。base class construction(基類構造)期間,virtual functions(虛擬函數)從來不會 go down(向下匹配)到 derived classes(派生類)。取而代之的是,那個 object (對象)的行為好像它就是 base type(基類型)。非正式地講,base class construction(基類構造)期間,virtual functions(虛擬函數)被禁止。
這個表面上看起來匪夷所思的行為存在一個很好的理由。因為 base class constructors(基類構造函數)在 derived class constructors(派生類構造函數)之前執行,當 base class constructors(基類構造函數)運行時,derived class data members(派生類數據成員)還沒有被初始化。如果 base class construction(基類構造)期間 virtual functions(虛擬函數)的調用 went down(向下匹配)到 derived classes(派生類),derived classes(派生類)的函數差不多總會涉及到 local data members(局部數據成員),但是那些 data members(數據成員)至此還沒有被初始化。這就會為 undefined behavior(未定義行為)和通宵達旦的調試噩夢開了一張通行證。調用涉及到一個 object(對象)還沒有被初始化的構件自然是危險的,所以 C++ 告訴你此路不通。
實際上還有比這更基本的原理。在一個 derived class object(派生類對象)的 base class construction(基類構造)期間,object(對象)的類型是 base class(基類)的類型。不僅 virtual functions(虛擬函數)會解析到 base class(基類),而且用到 runtime type information(運行時類型信息)的語言構件(例如,dynamic_cast(參見 Item 27)和 typeid),也會將那個 object(對象)視為 base class type(基類類型)。在我們的例子中,當 Transaction 的 constructor(構造函數)運行到初始化一個 BuyTransaction object(對象)的 base class(基類)部分時,那個 object(對象)的是 Transaction 類型。C++ 的每一個構件將以如下眼光來看待它,而且這種看法是合理的:這個 object(對象)的 BuyTransaction-specific 的構件還沒有被初始化,所以對它們視若無睹是最安全的。直到 derived class constructor(派生類構造函數)的執行開始之前,一個 object(對象)不會成為一個 derived class object(派生類對象)。
同樣的推理也適用于 destruction(析構)。一旦 derived class destructor(派生類析構函數)運行,這個 object(對象)的 derived class data members(派生類數據成員)就呈現為未定義的值,所以 C++ 就將它們視為不再存在。在進入 base class destructor(基類析構函數)時,這個 object(對象)就成為一個 base class object(基類對象),C++ 的所有構件—— virtual functions(虛擬函數),dynamic_casts 等——都以此看待它。
在上面的示例代碼中,Transaction 的 constructor(構造函數)造成了對一個 virtual functions(虛擬函數)的一次直接調用,是對本 Item 的指導建議的顯而易見的違背。這一違背是如此顯見,以致一些編譯器會給出一個關于它的警告。(另一些則不會。參見 Item 53 對于警告的討論。)即使沒有這樣的一個警告,這個問題也幾乎肯定會在運行之前暴露出來,因為 logTransaction 函數在 Transaction 中是 pure virtual(純虛擬)的。除非它被定義(不太可能,但確實可能——參見 Item 34),否則程序將無法連接:連接程序無法找到 Transaction::logTransaction 的必要的實現。
在 construction(構造)或 destruction(析構)期間調用 virtual functions(虛擬函數)的問題并不總是如此容易被察覺。如果 Transaction 有多個 constructors(構造函數),每一個都必須完成一些相同的工作,軟件工程為避免代碼重復,將共通的 initialization(初始化)代碼,包括對 logTransaction 的調用,放入一個 private non-virtual initialization function(私有非虛擬初始化函數)中,叫做 init:
```
class Transaction {
public:
Transaction()
{ init(); } // call to non-virtual...
virtual void logTransaction() const = 0;
...
private:
void init()
{
...
logTransaction(); // ...that calls a virtual!
}
};
```
這個代碼在概念上和早先那個版本相同,但是它更陰險,因為一般來說它會躲過編譯器和連接程序的抱怨。在這種情況下,因為 logTransaction 在 Transaction 中是 pure virtual(純虛的),在 pure virtual(純虛)被調用時,大多數 runtime systems(運行時系統)會異常中止那個程序(一般會對此結果給出一條消息)。然而,如果 logTransaction 在 Transaction 中是一個 "normal" virtual function(“常規”虛擬函數)(也就是說,not pure virtual(非純虛擬的)),而且帶有一個實現,那個版本將被調用,程序會繼續一路小跑,讓你想象不出為什么在 derived class object(派生類對象)被創建的時候會調用 logTransaction 的錯誤版本。避免這個問題的唯一辦法就是確保你的 constructors(構造函數)或 destructors(析構函數)決不在被創建或析構的 object(對象)上調用 virtual functions(虛擬函數),它們所調用的全部函數也要服從同樣的約束。
但是,你如何確保在每一次 Transaction hierarchy(繼承體系)中的一個 object(對象)被創建時,都會調用 logTransaction 的正確版本呢?顯然,在 Transaction constructor(s)(構造函數)中在這個 object(對象)上調用 virtual functions(虛擬函數)的做法是錯誤的。
有不同的方法來解決這個問題。其中之一是將 Transaction 中的 logTransaction 轉變為一個 non-virtual function(非虛擬函數),這就需要 derived class constructors(派生類構造函數)將必要的日志信息傳遞給 Transaction constructor(構造函數)。那個函數就可以安全地調用 non-virtual(非虛擬)的 logTransaction。如下:
```
class Transaction {
public:
explicit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo) const; // now a non-
// virtual func
...
};
Transaction::Transaction(const std::string& logInfo)
{
...
logTransaction(logInfo); // now a non-
} // virtual call
class BuyTransaction: public Transaction {
public:
BuyTransaction( parameters )
: Transaction(createLogString( parameters )) // pass log info
{ ... } // to base class
... // constructor
private:
static std::string createLogString( parameters );
};
```
換句話說,由于你不能在 base classes(基類)的 construction(構造)過程中使用 virtual functions(虛擬函數)向下匹配,你可以改為讓 derived classes(派生類)將必要的構造信息上傳給 base class constructors(基類構造函數)作為補償。
在此例中,注意 BuyTransaction 中那個 (private) static 函數 createLogString 的使用。使用一個輔助函數創建一個值傳遞給 base class constructors(基類構造函數),通常比通過在 member initialization list(成員初始化列表)給 base class(基類)它所需要的東西更加便利(也更加具有可讀性)。將那個函數做成 static,就不會有偶然觸及到一個新生的 BuyTransaction object(對象)的 as-yet-uninitialized data members(仍未初始化的數據成員)的危險。這很重要,因為實際上那些 data members(數據成員)處在一個未定義狀態,這就是為什么在 base class(基類)construction(構造)和 destruction(析構)期間調用 virtual functions(虛擬函數)不能首先向下匹配到 derived classes(派生類)的原因。
Things to Remember
* 在 construction(構造)或 destruction(析構)期間不要調用 virtual functions(虛擬函數),因為這樣的調用不會轉到比當前執行的 constructor(構造函數)或 destructor(析構函數)所屬的 class(類)更深層的 derived class(派生類)。
- 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 映射