# Item 40: 謹慎使用 multiple inheritance(多繼承)
作者:Scott Meyers
譯者:fatalerror99 (iTePub's Nirvana)
發布:http://blog.csdn.net/fatalerror99/
觸及 multiple inheritance (MI)(多繼承)的時候,C++ 社區就會鮮明地分裂為兩個基本的陣營。一個陣營認為如果 single inheritance (SI)(單繼承)是有好處的,multiple inheritance(多繼承)一定更有好處。另一個陣營認為 single inheritance(單繼承)有好處,但是多繼承引起的麻煩使它得不償失。在這個 Item 中,我們的主要目的是理解在 MI 問題上的這兩種看法。
首要的事情之一是要承認當將 MI 引入設計領域時,就有可能從多于一個的 base class(基類)中繼承相同的名字(例如,函數,typedef,等等)。這就為歧義性提供了新的時機。例如:
```
class BorrowableItem { // something a library lets you borrow
public:
void checkOut(); // check the item out from the library
...
};
class ElectronicGadget {
private:
bool checkOut() const; // perform self-test, return whether
... // test succeeds
};
class MP3Player: // note MI here
public BorrowableItem, // (some libraries loan MP3 players)
public ElectronicGadget
{ ... }; // class definition is unimportant
MP3Player mp;
mp.checkOut(); // ambiguous! which checkOut?
```
注意這個例子,即使兩個函數中只有一個是可訪問的,對 checkOut 的調用也是有歧義的。(checkOut 在 BorrowableItem 中是 public(公有)的,但在 ElectronicGadget 中是 private(私有)的。)這與 C++ 解析 overloaded functions(重載函數)調用的規則是一致的:在看到一個函數的是否可訪問之前,C++ 首先確定與調用匹配最好的那個函數。只有在確定了 best-match function(最佳匹配函數)之后,才檢查可訪問性。這目前的情況下,兩個 checkOuts 具有相同的匹配程度,所以就不存在最佳匹配。因此永遠也不會檢查到 ElectronicGadget::checkOut 的可訪問性。
為了消除歧義性,你必須指定哪一個 base class(基類)的函數被調用:
```
mp.BorrowableItem::checkOut(); // ah, that checkOut...
```
當然,你也可以嘗試顯式調用 ElectronicGadget::checkOut,但這樣做會有一個 "you're trying to call a private member function"(你試圖調用一個私有成員函數)錯誤代替歧義性錯誤。
multiple inheritance(多繼承)僅僅意味著從多于一個的 base class(基類)繼承,但是在還有 higher-level base classes(更高層次基類)的 hierarchies(繼承體系)中出現 MI 也并不罕見。這會導致有時被稱為 "deadly MI diamond"(致命的多繼承菱形)的后果。
```
class File { ... };
class InputFile: public File { ... };
class OutputFile: public File { ... };
class IOFile: public InputFile,
public OutputFile
{ ... };
```

你擁有一個“在一個 base class(基類)和一個 derived class(派生類)之間有多于一條路徑的 inheritance hierarchy(繼承體系)”(就像上面在 File 和 IOFile 之間,有通過 InputFile 和 OutputFile 的兩條路徑)的任何時候,你都必須面對是否需要為每一條路徑復制 base class(基類)中的 data members(數據成員)的問題。例如,假設 File class 有一個 data members(數據成員)fileName。IOFile 中應該有這個 field(字段)的多少個拷貝呢?一方面,它從它的每一個 base classes(基類)繼承一個拷貝,這就暗示 IOFile 應該有兩個 fileName data members(數據成員)。另一方面,簡單的邏輯告訴我們一個 IOFile object(對象)應該僅有一個 file name(文件名),所以通過它的兩個 base classes(基類)繼承來的 fileName field(字段)不應該被復制。
C++ 在這個爭議上沒有自己的立場。它恰當地支持兩種選項,雖然它的缺省方式是執行復制。如果那不是你想要的,你必須讓這個 class(類)帶有一個 virtual base class(虛擬基類)的數據(也就是 File)。為了做到這一點,你要讓從它直接繼承的所有的 classes(類)使用 virtual inheritance(虛擬繼承):
```
class File { ... };
class InputFile: virtual public File { ... };
class OutputFile: virtual public File { ... };
class IOFile: public InputFile,
public OutputFile
{ ... };
```

標準 C++ 庫包含一個和此類似的 MI hierarchy(繼承體系),只是那個 classes(類)是 class templates(類模板),名字是 basic_ios,basic_istream,basic_ostream 和 basic_iostream,而不是 File,InputFile,OutputFile 和 IOFile。
從正確行為的觀點看,public inheritance(公有繼承)應該總是 virtual(虛擬)的。如果這是唯一的觀點,規則就變得簡單了:你使用 public inheritance(公有繼承)的任何時候,都使用 virtual public inheritance(虛擬公有繼承)。唉,正確性不是唯一的視角。避免 inherited fields(繼承來的字段)復制需要在編譯器的一部分做一些 behind-the-scenes legerdemain(幕后的戲法),而結果是從使用 virtual inheritance(虛擬繼承)的 classes(類)創建的 objects(對象)通常比不使用 virtual inheritance(虛擬繼承)的要大。訪問 virtual base classes(虛擬基類)中的 data members(數據成員)也比那些 non-virtual base classes(非虛擬基類)中的要慢。編譯器與編譯器之間有一些細節不同,但基本的要點很清楚:virtual inheritance costs(虛擬繼承要付出成本)。
它也有一些其它方面的成本。支配 initialization of virtual base classes(虛擬基類初始化)的規則比 non-virtual bases(非虛擬基類)的更加復雜而且更不直觀。初始化一個 virtual base(虛擬基)的職責由 hierarchy(繼承體系)中 most derived class(層次最低的派生類)承擔。這個規則中包括的含義:(1) 從需要 initialization(初始化)的 virtual bases(虛擬基)派生的 classes(類)必須知道它們的 virtual bases(虛擬基),無論它距離那個 bases(基)有多遠;(2) 當一個新的 derived class(派生類)被加入繼承體系時,它必須為它的 virtual bases(虛擬基)(包括直接的和間接的)承擔 initialization responsibilities(初始化職責)。
我對于 virtual base classes(虛擬基類)(也就是 virtual inheritance(虛擬繼承))的建議很簡單。首先,除非必需,否則不要使用 virtual bases(虛擬基)。缺省情況下,使用 non-virtual inheritance(非虛擬繼承)。第二,如果你必須使用 virtual base classes(虛擬基類),試著避免在其中放置數據。這樣你就不必在意它的 initialization(初始化)(以及它的 turns out(清空),assignment(賦值))規則中的一些怪癖。值得一提的是 Java 和 .NET 中的 Interfaces(接口)不允許包含任何數據,它們在很多方面可以和 C++ 中的 virtual base classes(虛擬基類)相比照。
現在我們使用下面的 C++ Interface class(接口類)(參見 Item 31)來為 persons(人)建模:
```
class IPerson {
public:
virtual ~IPerson();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
};
```
IPerson 的客戶只能使用 IPerson 的 pointers(指針)和 references(引用)進行編程,因為 abstract classes(抽象類)不能被實例化。為了創建能被當作 IPerson objects(對象)使用的 objects(對象),IPerson 的客戶使用 factory functions(工廠函數)(再次參見 Item 31)instantiate(實例化)從 IPerson 派生的 concrete classes(具體類):
```
// factory function to create a Person object from a unique database ID;
// see Item 18 for why the return type isn't a raw pointer
std::tr1::shared_ptr<IPerson> makePerson(DatabaseID personIdentifier);
// function to get a database ID from the user
DatabaseID askUserForDatabaseID();
DatabaseID id(askUserForDatabaseID());
std::tr1::shared_ptr<IPerson> pp(makePerson(id)); // create an object
// supporting the
// IPerson interface
... // manipulate *pp via
// IPerson's member
// functions
```
但是 makePerson 怎樣創建它返回的 pointers(指針)所指向的 objects(對象)呢?顯然,必須有一些 makePerson 可以實例化的從 IPerson 派生的 concrete class(具體類)。
假設這個 class(類)叫做 CPerson。作為一個 concrete class(具體類),CPerson 必須提供它從 IPerson 繼承來的 pure virtual functions(純虛擬函數)的 implementations(實現)。它可以從頭開始寫,但利用包含大多數或全部必需品的現有組件更好一些。例如,假設一個老式的 database-specific class(老式的數據庫專用類)PersonInfo 提供了 CPerson 所需要的基本要素:
```
class PersonInfo {
public:
explicit PersonInfo(DatabaseID pid);
virtual ~PersonInfo();
virtual const char * theName() const;
virtual const char * theBirthDate() const;
...
private:
virtual const char * valueDelimOpen() const; // see
virtual const char * valueDelimClose() const; // below
...
};
```
你可以看出這是一個老式的 class(類),因為 member functions(成員函數)返回 const char\*s 而不是 string objects(對象)。盡管如此,如果鞋子合適,為什么不穿呢?這個 class(類)的 member functions(成員函數)的名字暗示結果很可能會非常合適。
你突然發現 PersonInfo 是設計用來幫助以不同的格式打印 database fields(數據庫字段)的,每一個字段的值的開始和結尾通過指定的字符串定界。缺省情況下,字段值開始和結尾定界符是方括號,所以字段值 "Ring-tailed Lemur" 很可能被安排成這種格式:
```
[Ring-tailed Lemur]
```
根據方括號并非滿足 PersonInfo 的全體客戶的期望的事實,virtual functions(虛擬函數)valueDelimOpen 和 valueDelimClose 允許 derived classes(派生類)指定它們自己的開始和結尾定界字符串。PersonInfo 的 member functions(成員函數)的 implementations(實現)調用這些 virtual functions(虛擬函數)在它們返回的值上加上適當的定界符。作為一個例子使用 PersonInfo::theName,代碼如下:
```
const char * PersonInfo::valueDelimOpen() const
{
return "["; // default opening delimiter
}
const char * PersonInfo::valueDelimClose() const
{
return "]"; // default closing delimiter
}
const char * PersonInfo::theName() const
{
// reserve buffer for return value; because this is
// static, it's automatically initialized to all zeros
static char value[Max_Formatted_Field_Value_Length];
// write opening delimiter
std::strcpy(value, valueDelimOpen());
append to the string in value this object's name field (being careful
to avoid buffer overruns!)
// write closing delimiter
std::strcat(value, valueDelimClose());
return value;
}
```
有人可能會質疑 PersonInfo::theName 的陳舊的設計(特別是一個 fixed-size static buffer(固定大小靜態緩沖區)的使用,這樣的東西發生 overrun(越界)和 threading(線程)問題是比較普遍的——參見 Item 21),但是請把這樣的問題放到一邊而注意這里:theName 調用 valueDelimOpen 生成它要返回的 string(字符串)的開始定界符,然后它生成名字值本身,然后它調用 valueDelimClose。
因為 valueDelimOpen 和 valueDelimClose 是 virtual functions(虛擬函數),theName 返回的結果不僅依賴于 PersonInfo,也依賴于從 PersonInfo 派生的 classes(類)。
對于 CPerson 的實現者,這是好消息,因為當細讀 IPerson documentation(文檔)中的 fine print(晦澀的條文)時,你發現 name 和 birthDate 需要返回未經修飾的值,也就是,不允許有定界符。換句話說,如果一個人的名字叫 Homer,對那個人的 name 函數的一次調用應該返回 "Homer",而不是 "[Homer]"。
CPerson 和 PersonInfo 之間的關系是 PersonInfo 碰巧有一些函數使得 CPerson 更容易實現。這就是全部。因而它們的關系就是 is-implemented-in-terms-of,而我們知道有兩種方法可以表現這一點:經由 composition(復合)(參見 Item 38)和經由 private inheritance(私有繼承)(參見 Item 39)。Item 39 指出 composition(復合)是通常的首選方法,但如果 virtual functions(虛擬函數)要被重定義,inheritance(繼承)就是必不可少的。在當前情況下,CPerson 需要重定義 valueDelimOpen 和 valueDelimClose,所以簡單的 composition(復合)做不到。最直截了當的解決方案是讓 CPerson 從 PersonInfo privately inherit(私有繼承),雖然 Item 39 說過只要多做一點工作,則 CPerson 也能用 composition(復合)和 inheritance(繼承)的組合有效地重定義 PersonInfo 的 virtuals(虛擬函數)。這里,我們用 private inheritance(私有繼承)。
但是 CPerson 還必須實現 IPerson interface(接口),而這被稱為 public inheritance(公有繼承)。這就引出一個 multiple inheritance(多繼承)的合理應用:組合 public inheritance of an interface(一個接口的公有繼承)和 private inheritance of an implementation(一個實現的私有繼承):
```
class IPerson { // this class specifies the
public: // interface to be implemented
virtual ~IPerson();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
};
class DatabaseID { ... }; // used below; details are
// unimportant
class PersonInfo { // this class has functions
public: // useful in implementing
explicit PersonInfo(DatabaseID pid); // the IPerson interface
virtual ~PersonInfo();
virtual const char * theName() const;
virtual const char * theBirthDate() const;
virtual const char * valueDelimOpen() const;
virtual const char * valueDelimClose() const;
...
};
class CPerson: public IPerson, private PersonInfo { // note use of MI
public:
explicit CPerson( DatabaseID pid): PersonInfo(pid) {}
virtual std::string name() const // implementations
{ return PersonInfo::theName(); } // of the required
// IPerson member
virtual std::string birthDate() const // functions
{ return PersonInfo::theBirthDate(); }
private: // redefinitions of
const char * valueDelimOpen() const { return ""; } // inherited virtual
const char * valueDelimClose() const { return ""; } // delimiter
};
```
在 UML 中,這個設計看起來像這樣:

這個例子證明 MI 既是有用的,也是可理解的。
時至今日,multiple inheritance(多繼承)不過是 object-oriented toolbox(面向對象工具箱)里的又一種工具而已,典型情況下,它的使用和理解更加復雜,所以如果你得到一個或多或少等同于一個 MI 設計的 SI 設計,則 SI 設計總是更加可取。如果你能拿出來的僅有的設計包含 MI,你應該更加用心地考慮一下——總會有一些方法使得 SI 也能做到。但同時,MI 有時是最清晰的,最易于維護的,最合理的完成工作的方法。在這種情況下,毫不畏懼地使用它。只是要確保謹慎地使用它。
Things to Remember
* multiple inheritance(多繼承)比 single inheritance(單繼承)更復雜。它能導致新的歧義問題和對 virtual inheritance(虛擬繼承)的需要。
* virtual inheritance(虛擬繼承)增加了 size(大小)和 speed(速度)成本,以及 initialization(初始化)和 assignment(賦值)的復雜度。當 virtual base classes(虛擬基類)沒有數據時它是最適用的。
* multiple inheritance(多繼承)有合理的用途。一種方案涉及組合從一個 Interface class(接口類)的 public inheritance(公有繼承)和從一個有助于實現的 class(類)的 private inheritance(私有繼承)。
- 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 映射