Effective C++筆記: 繼承和麪向對象設計(三)

 

Item 38: 通過 composition(複合)塑模出 "has-a" "is-implemented-in-terms-of"(根據某物實現出)

composition(複合)是類型之間的一種關係,表示某種類型的對象內含其他類型的對象。例如:

class Address { ... };             // where someone lives

class PhoneNumber { ... };

class Person {
public:
  ...

private:
 
std::string name;               // composed object
 
Address address;                // ditto
 
PhoneNumber voiceNumber;        // ditto
 
PhoneNumber faxNumber;          // ditto
};

此例之中,Person objects(對象)由 stringAddress,和 PhoneNumber objects(對象)組成。術語 composition(複合)有很多同義詞。它也可以稱爲 layering(分層)containment(內含)aggregation(聚合),和 embedding(內嵌)

 

區分has-a is-implemented-in-terms-of

上面的 Person class(類)示範了 has-a(有一個)的關係。一個 Person object(對象)has a(有一個)名字,一個地址,以及語音和傳真電話號碼。你不能說一個人 is a(是一個)名字或一個人 is an(是一個)地址。你可以說一個人 has a(有一個)名字和 has an(有一個)地址。

is-implemented-in-terms-of的例子是,比如,你需要一個類的模板來表現相當小的 objects(對象)的 sets,也就是說,排除重複的集合。你決定複用標準的 C++ 庫中list template(模板),具體地說,你決定讓你的新的 Set template(模板)從 list 繼承。也就是說,Set<T> 將從 list<T> 繼承。畢竟,在你的實現中,一個 Set object(對象)實際上就是一個 list object(對象)。於是,你就像這樣聲明你的 Set template(模板):

template<typename T>                       // the wrong way to use list for Set
class Set: public std::list<T> { ... };

在這裏,看起來每件事情都很好。但實際上有一個很大的錯誤。就像 Item 32 中的解釋,如果 D is-a(是一個)B對於 B 成立的每一件事情對 D 也成立。然而,一個 list object(對象)可以包含重複,所以如果值 3051 被插入一個 list<int> 兩次,那個 list 將包含 3051 的兩個拷貝。與此對照,一個 Set 不可以包含重複,所以如果值 3051 被插入一個 Set<int> 兩次,那個 set 只包含該值的一個拷貝。因此一個 Set is-a(是一個)list 是不正確的,因爲對 list objects(對象)成立的某些事情對 Set objects(對象)不成立。

因爲這兩個 classes(類)之間的關係不是 is-a(是一個),public inheritance(公有繼承)不是模擬這個關係的正確方法。正確的方法是認識到一個 Set object(對象)可以 be implemented in terms of a list object(是根據一個 list 對象實現的)

template<class T>                   // the right way to use list for Set
class Set {
public:
  bool member(const T& item) const;

  void insert(const T& item);
  void remove(const T& item);

  std::size_t size() const;

private:
 
std::list<T> rep;                 // representation for Set data
};

 

總結:

composition(複合)與 public inheritance(公有繼承)的意義完全不同。

application domain(應用領域)中,composition(複合)意味着 has-a(有一個)。在 implementation domain(實現領域)中意味着 is-implemented-in-terms-of(是根據……實現的)。

 

Item 39: 謹慎使用 private inheritance(私有繼承)

Item 32 論述了 C++ public inheritance(公有繼承)視爲一個 is-a 關係。當給定一個 hierarchy(繼承體系),其中有一個 class Student 從一個 class Person 公有繼承,編譯器在必要時刻(爲了成功調用一個函數)要將 Students 隱式轉型爲 Persons。但對private inheritance(私有繼承)情況會如何呢?

class Person { ... };
class Student: private Person { ... };     // inheritance is now private

void eat(const Person& p);                 // anyone can eat

void study(const Student& s);              // only students study

Person p;                                  // p is a Person
Student s;                                 // s is a Student

eat(p);                                    // fine, p is a Person

eat(s);                                    // error! a Student isn't a Person

很明顯,private inheritance(私有繼承)不意味着 is-a。那麼它意味着什麼呢?

 

private inheritance(私有繼承)的兩個規則:

1. public inheritance(公有繼承)相反,如果 classes(類)之間的 inheritance relationship(繼承關係)是 private(私有)的,編譯器通常不會將一個 derived class object(派生類對象)(諸如 Student)轉型爲一個 base class object(基類對象)(諸如 Person)。這就是爲什麼爲 object(對象)s 調用 eat 會失敗。

2.從一個 private base class(私有基類)繼承的 members(成員)會成爲 derived class(派生類)的 private members(私有成員),即使它們在 base class(基類)中是 protected(保護)的或 public(公有)的。

 

private inheritance(私有繼承)意味着 is-implemented-in-terms-of(是根據……實現的)。如果你使 class(類)D class(類)B 私有繼承,你這樣做是因爲你對於利用在 class(類)B 中才可用的某些特性感興趣,而不是因爲在 types(類型)B types(類型)D objects(對象)之間有什麼概念上的關係。同樣地,private inheritance(私有繼承)純粹是一種實現技術。(這也就是爲什麼你從一個 private base class(私有基類)繼承的每一件東西都在你的 class(類)中變成 private(私有)的原因:它全部都是實現的細節。)利用 Item 34 中提出的條款,private inheritance(私有繼承)意味着只有 implementation(實現)應該被繼承;interface(接口)應該被忽略。如果 D B 私有繼承,它就意味着 D objects are implemented in terms of B objectsD 對象是根據 B 對象實現的),沒有更多了。private inheritance(私有繼承)在 software design(軟件設計)期間沒有任何意義,只在 software implementation(軟件實現)期間纔有。

 

私有繼承與複合:

private inheritance(私有繼承)意味着 is-implemented-in-terms-of(是根據……實現的)的事實有一點混亂,因爲 Item 38 指出 composition(複合)也有同樣的含義。你怎麼預先在它們之間做出選擇呢?答案很簡單:儘可能使用composition(複合),只有在絕對必要的時候才用 private inheritance(私有繼承)。

 

例如,我們要給Widget class加入一個Tick功能,假設我們已經有了一個Timer class

class Timer {
public:
  explicit Timer(int tickFrequency);
   virtual void onTick() const;          // automatically called for each tick
  ...
};

爲了給 Widget 重定義 Timer 中的一個 virtual function(虛擬函數),Widget 必須從 Timer 繼承。但是 public inheritance(公有繼承)在這種情況下不合適。Widget is-a(是一個)Timer 不成立。Widget 的客戶不應該能夠在一個 Widget 上調用 onTick,因爲在概念上那不是的 Widget  interface(接口)的一部分。允許這樣的函數調用將使客戶更容易誤用 Widget interface(接口),這是一個對 Item 18 的關於使接口易於正確使用,而難以錯誤使用的建議的明顯違背。public inheritance(公有繼承)在這裏不是正確的選項。

因此我們就 inherit privately(祕密地繼承):

class Widget: private Timer {
private:
  virtual void onTick() const;           // look at Widget usage data, etc.
  ...
};

通過 private inheritance(私有繼承)的能力,Timer public(公有)onTick 函數在 Widget 中變成 private(私有)的,而且在我們重新聲明它的時候,也把它保留在那裏。重複一次,將 onTick 放入 public interface(公有接口)將誤導客戶認爲他們可以調用它,而這違背了 Item 18

這是一個很好的設計,但值得一提的是,private inheritance(私有繼承)並不是絕對必要的。如果我們決定用 composition(複合)來代替,也是可以的。我們僅需要在我們從 Timer 公有繼承來的 Widget 內聲明一個 private nested class(私有嵌套類),在那裏重定義 onTick,並在 Widget 中放置一個那個類型的 object(對象)。以下就是這個方法的概要:

class Widget {
private:
  class WidgetTimer: public Timer {
  public:
    virtual void onTick() const;
    ...
  };
   WidgetTimer timer;
  ...
};

public inheritance(公有繼承)加 composition(複合)而不用 private inheritance(私有繼承)的兩個原因。

首先,你可能要做出允許 Widget derived classes(派生類)的設計,但是你還可能要禁止 derived classes(派生類)重定義 onTick。如果 Widget Timer 繼承,那是不可能的,即使 inheritance(繼承)是 private(私有)的也不行。(回憶 Item 35 derived classes(派生類)可以重定義 virtual functions(虛擬函數),即使調用它們是不被允許的。)但是如果 WidgetTimer Widget 中是 private(私有)的而且是從 Timer 繼承的,Widget derived classes(派生類)就不能訪問 WidgetTimer,因此就不能從它繼承或重定義它的 virtual functions(虛擬函數)。如果你曾在 Java C# 中編程並且錯過了禁止 derived classes(派生類)重定義 virtual functions(虛擬函數)的能力(也就是,Java final methods(方法)和 C# sealed),現在你有了一個在 C++ 中的到類似行爲的想法。

第二,你可能需要最小化 Widget compilation dependencies(編譯依賴)。如果 Widget Timer 繼承,在 Widget 被編譯的時候 Timer definition(定義)必須是可用的,所以定義 Widget 的文件可能不得不 #include Timer.h。另一方面,如果 WidgetTimer 移出 Widget Widget 只包含一個指向一個 WidgetTimer pointer(指針),Widget 就可以只需要 WidgetTimer class(類)的一個簡單的 declaration(聲明);爲了使用 Timer 它不需要 #include 任何東西。對於大型系統,這樣的隔離可能非常重要(關於 minimizing compilation dependencies(最小化編譯依賴)的細節,參見 Item 31)。

 

EBO empty base optimization空白基類最優化)

一個空類是指一個類,沒有 non-static data members(非靜態數據成員);沒有 virtual functions(虛函數)(因爲存在這樣的函數會在每一個 object(對象)中增加一個 vptr ——參見 Item 7);也沒有 virtual base classes(虛擬基類)(因爲這樣的 base classes(基類)也會引起 size overhead(大小成本)——參見 Item 40)。在理論上,這樣的 empty classes(空類)的 objects(對象)應該不佔用空間,因爲沒有 per-object(逐對象)的數據需要存儲。然而,由於 C++ 天生的技術上的原因,freestanding objects(獨立對象)必須有 non-zero size(非零大小),所以如果你這樣做,

class Empty {};                      // has no data, so objects should
                                     // use no memory
class HoldsAnInt {                   // should need only space for an int
private:
  int x;
  Empty e;                           // should require no memory
};

你將發現 sizeof(HoldsAnInt) > sizeof(int);一個 Empty data member(空數據成員)需要存儲。對以大多數編譯器,sizeof(Empty) 1,這是因爲 C++ 法則反對 zero-size freestanding objects(獨立對象)一般是通過在 "empty" objects對象)中插入一個 char 完成的。然而,alignment requirements(對齊需求)(參見 Item 50)可能促使編譯器向類似 HoldsAnInt classes(類)中增加填充物,所以,很可能 HoldsAnInt objects 得到的不僅僅是一個 char 的大小,實際上它們可能會擴張到足以佔據第二個 int 的位置。(在我測試過的所有編譯器上,這毫無例外地發生了。)

但是也許你已經注意到我小心翼翼地說 "freestanding" objects獨立對象)必然不會有 zero size。這個約束不適用於 base class parts of derived class objects(派生類對象的基類構件),因爲它們不是獨立的。如果你用從 Empty 繼承代替包含一個此類型的 object(對象),

class HoldsAnInt: private Empty {
private:
  int x;
};

你幾乎總是會發現 sizeof(HoldsAnInt) == sizeof(int)

在實踐中,"empty" classes類)並不真的爲空。雖然他們絕對不會有 non-static data members(非靜態數據成員),但它們經常會包含 typedefsenums(枚舉),static data members(靜態數據成員),或 non-virtual functions(非虛擬函數)。STL 有很多包含有用的 members(成員)(通常是 typedefs)的專門的 empty classes(空類),包括 base classes(基類)unary_function binary_functionuser-defined function objects(用戶定義函數對象)通常從這些 classes(類)繼承而來。感謝 EBO 的普遍實現,這樣的繼承很少增加 inheriting classes(繼承來的類)的大小。

 

總結:

private inheritance(私有繼承)意味着 is-implemented-in-terms of(是根據……實現的)。它通常比 composition(複合)更低級,但當一個 derived class(派生類)需要訪問 protected base class members(保護基類成員)或需要重定義 inherited virtual functions(繼承來的虛擬函數)時,這麼設計是合理的。

composition(複合)不同,private inheritance(私有繼承)能使 empty base optimization(空基優化)有效。這對於致力於最小化 object sizes(對象大小)的庫開發者來說可能是很重要的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章