程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C++箴言:謹慎使用私有繼承

C++箴言:謹慎使用私有繼承

編輯:關於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(對象大小)的庫開發者來說可能是很重要的。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved