C++之私有繼承

在《C++箴言:確保公開繼承模擬“is-a”》一文中論述了 C++ 將 public inheritance(公有繼承)視爲一個 is-a 關係。當給定一個 hierarchy(繼承體系),其中有一個 class Student 從一個 class Person 公有繼承,當爲一個函數調用的成功而有必要時,需要將 Students 隱式轉型爲 Persons,它通過向編譯器展示來做到這一點。用 private inheritance(私有繼承)代替 public 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(私有繼承)有怎樣的行爲呢?”好吧,支配 private inheritance(私有繼承)的第一個規則你只能從動作中看到:與 public inheritance(公有繼承)對照,如果 classes(類)之間的 inheritance relationship(繼承關係)是 private(私有)的,編譯器通常不會將一個 derived class object(派生類對象)(諸如 Student)轉型爲一個 base class object(基類對象)(諸如 Person)。這就是爲什麼爲 object(對象)s 調用 eat 會失敗。第二個規則是從一個 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(私有)的原因:它全部都是實現的細節。)利用《
接口繼承和實現繼承》中提出的條款,private inheritance(私有繼承)意味着只有 implementation(實現)應該被繼承;interface(接口)應該被忽略。

  如果 D 從 B 私有繼承,它就意味着 D objects are implemented in terms of B objects(D 對象是根據 B 對象實現的),沒有更多了。private inheritance(私有繼承)在 software design(軟件設計)期間沒有任何意義,只在 software implementation(軟件實現)期間纔有。 private inheritance(私有繼承)意味着 is-implemented-in-terms-of(是根據……實現的)的事實有一點混亂,正如《
通過composition模擬“has-a”》一文中所指出的 composition(複合)也有同樣的含義。你怎麼預先在它們之間做出選擇呢?答案很簡單:只要你能就用 composition(複合),只有在絕對必要的時候才用 private inheritance(私有繼承)。什麼時候是絕對必要呢?主要是當 protected members(保護成員)和/或 virtual functions(虛擬函數)摻和進來的時候,另外還有一種與空間相關的極端情況會使天平向 private inheritance(私有繼承)傾斜。我們稍後再來操心這種極端情況。

  畢竟,它只是一種極端情況。 假設我們工作在一個包含 Widgets 的應用程序上,而且我們認爲我們需要更好地理解 Widgets 是怎樣被使用的。例如,我們不僅要知道 Widget member functions(成員函數)被調用的頻度,還要知道 call ratios(調用率)隨着時間的流逝如何變化。帶有清晰的執行階段的程序在不同的執行階段可以有不同的行爲側重。例如,一個編譯器在解析階段對函數的使用與優化和代碼生成階段就有很大的不同。

  我們決定修改 Widget class 以持續跟蹤每一個 member function(成員函數)被調用了多少次。在運行時,我們可以週期性地檢查這一信息,與每一個 Widget 的這個值相伴的可能還有我們覺得有用的其它數據。爲了進行這項工作,我們需要設立某種類型的 timer(計時器),以便在到達收集用法統計的時間時我們可以知道。

  儘可能複用已有代碼,而不是寫新的代碼,我在我的工具包中翻箱倒櫃,而且滿意地找到下面這個 class(類):

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

  這正是我們要找的:一個我們能夠根據我們的需要設定 tick 頻率的 Timer object,而在每次 tick 時,它調用一個 virtual function(虛擬函數)。我們可以重定義這個 virtual function(虛擬函數)以便讓它檢查 Widget 所在的當前狀態。很完美!
爲了給 Widget 重定義 Timer 中的一個 virtual function(虛擬函數),Widget 必須從 Timer 繼承。但是 public inheritance(公有繼承)在這種情況下不合適。Widget is-a(是一個)Timer 不成立。Widget 的客戶不應該能夠在一個 Widget 上調用 onTick,因爲在概念上那不是的 Widget 的 interface(接口)的一部分。允許這樣的函數調用將使客戶更容易誤用 Widget 的 interface(接口),這是一個對《使接口易於正確使用難錯誤使用》中的關於“使接口易於正確使用,而難以錯誤使用”的建議的明顯違背。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(公有接口)將誤導客戶認爲他們可以調用它,而這違背了我在《使接口易於正確使用難錯誤使用》。

  這是一個很好的設計,但值得注意的是,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;
 ...
};

  這個設計比只用了 private inheritance(私有繼承)的那一個更復雜,因爲它包括 (public) inheritance((公有)繼承)和 composition(複合)兩者,以及一個新 class (WidgetTimer) 的引入。老實說,我出示它主要是爲了提醒你有多於一條的道路通向一個設計問題,而且它也可以鍛鍊你自己你自己考慮多種方法(參見《C++箴言:最小化文件之間的編譯依賴》)。然而,我可以想到爲什麼你可能更願意用 public inheritance(公有繼承)加 composition(複合)而不用 private inheritance(私有繼承)的兩個原因。

  首先,你可能要做出允許 Widget 有 derived classes(派生類)的設計,但是你還可能要禁止 derived classes(派生類)重定義 onTick。如果 Widget 從 Timer 繼承,那是不可能的,即使 inheritance(繼承)是 private(私有)的也不行。(回憶《
C++箴言:考慮可選的虛擬函數的替代方法》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(最小化編譯依賴)的細節,參見《
C++箴言:最小化文件之間的編譯依賴》)。

  我早些時候談及 private inheritance(私有繼承)主要用武之地是當一個將要成爲 derived class(派生類)的類需要訪問將要成爲 base class(基類)的類的 protected parts(保護構件),或者希望重定義一個或多個它的 virtual functions(虛擬函數),但是 classes(類)之間的概念上的關係卻是 is-implemented-in-terms-of,而不是 is-a。然而,我也說過有一種涉及 space optimization(空間最優化)的極端情況可能會使你傾向於 private inheritance(私有繼承),而不是 composition(複合)。

  這個極端情況確實非常尖銳:它僅僅適用於你處理一個其中沒有數據的 class(類)的時候。這樣的 classes(類)沒有 non-static data members(非靜態數據成員);沒有 virtual functions(虛函數)(因爲存在這樣的函數會在每一個 object(對象)中增加一個 vptr ——參見《
C++箴言:多態基類中將析構函數聲明爲虛擬》);也沒有 virtual base classes(虛擬基類)(因爲這樣的 base classes(基類)也會引起 size overhead(大小成本))。在理論上,這樣的 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(對齊需求)可能促使編譯器向類似 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 base optimization (EBO)(空基優化)聞名,而且它已經被我測試過的所有編譯器實現。如果你是一個空間敏感的客戶的庫開發者,EBO 就值得了解。同樣值得了解的是 EBO 通常只在 single inheritance(單繼承)下才可行。支配 C++ object layout(C++ 對象佈局)的規則通常意味着 EBO 不適用於擁有多於一個 base(基)的 derived classes(派生類)。

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

  儘管如此,我們還是要回歸基礎。大多數 classes(類)不是空的,所以 EBO 很少會成爲 private inheritance(私有繼承)的一個合理的理由。此外,大多數 inheritance(繼承)相當於 is-a,而這正是 public inheritance(公有繼承)而非 private(私有)所做的事。composition(複合)和 private inheritance(私有繼承)兩者都意味着 is-implemented-in-terms-of(是根據……實現的),但是 composition(複合)更易於理解,所以你應該盡你所能使用它。

  private inheritance(私有繼承)更可能在以下情況中成爲一種設計策略,當你要處理的兩個 classes(類)不具有 is-a(是一個)的關係,而且其中的一個還需要訪問另一個的 protected members(保護成員)或需要重定義一個或更多個它的 virtual functions(虛擬函數)。甚至在這種情況下,我們也看到 public inheritance 和 containment 的混合使用通常也能產生你想要的行爲,雖然有更大的設計複雜度。謹慎使用 private inheritance(私有繼承)意味着在使用它的時候,已經考慮過所有的可選方案,只有它纔是你的軟件中明確表示兩個 classes(類)之間關係的最佳方法。

  Things to Remember

  ·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(對象大小)的庫開發者來說可能是很重要的。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章