標籤:高效c++ private繼承 c++ 繼承
- 條款38通過複合塑模樹has-a 或根據某物實現出
- 條款39明智而審慎的使用private繼承
條款38:通過複合塑模樹
has-a 或“根據某物實現出”
複合(composition)是類型之間的一種關係,一個類型的對象包含其他類型對象便是這種關係:
class Address{ …… };class PhoneNumber{ …… };class Person{public: ……private: std::string name; Address address; PhoneNumber mobilePhone;};
Person對象中包含string,Address,PhoneNumber對象,這就是複合。還有幾個同義字:layering(分層),containment(內含),aggregation(彙總),embedding(內嵌)。
條款 32中提到,public是is-a關係,複合是has-a(有一個)或is-implemented-in-terms-of(根據某物實現出)。在程式中,大概可以分為兩個領域(domains)。程式中對象相當於你所塑造現實世界中某物,例如地址、電話號碼,這樣的對象屬於應用域(application domain)。還有一些是實現細節上的人工複製品,例如緩衝區(buffers)、互斥器(mutexes)、尋找樹(search tree)等,這些是實現域(implementation domain)。當複合發生在應用域對象之間時,表現出has-a關係;發生在實現域表現出is-implemented-in-terms-of關係。
區分is-a和is-implemented-in-terms-of比較麻煩。通過一個例子來說明,假設你需要一個template,用來構造一組classes來表示不重複對象組成的sets。首先我們想到用標準程式庫提供的set template。
標準程式庫的set由平衡尋找樹(balance search tree)實現,每個元素使用了三個指標的額外開銷。這樣可以使尋找、插入、移除等操作時間複雜度為O(logN)(對數時間,logarithmic-time)。如果速度比空間重要,這樣做合理,但是如果空間比速度重要,那麼標準庫提供的set將不滿足我們需求。
set實現方法很多,可以在底層使用linked lists來實現,標準庫中有list template,於是我們複用它。
template<typename T>class Set: public std::list<T>{ ……};
上面看起來很美好,其實是錯誤的。條款 32曾說過,public繼承是is-a關係,即set是一種list並不對。例如set不能包含重複元素,但是list可以。
因為這兩個classes之間並非is-a關係,所以public繼承並不適用。set對象可以根據一個list對象來實現出來:
template<calss T>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;};
只要熟悉list,便很快可以實現上面幾個介面函數。
條款 18主張,介面容易被正確適用,不易被誤用。這裡沒有讓set遵循STL容器的協議,如果遵循的話,要為set添加許多東西,這就模糊了set和list之間的關係。這裡只是澄清set和list之間的關係。
總結
- 複合(composition)的意義和public繼承完全不同。
- 在應用域(application domain),複合意味has-a;在實現域(implementation domain),複合意味is-implemented-in-terms-of(根據某物實現出)。
條款39:明智而審慎的使用private繼承
public繼承是is-a關係,**條款**32曾講過並給出例子,如果把那個例子用private繼承會怎樣?
class Person{……};class Student: private Person{……};void eat(const Person& p);void study(const Student& s);Person p;Student s;eat(p);eat(s);
上的的eat(s)會出錯,因為private繼承不是is-a關係。如果繼承關係是private,那麼編譯器不會自動將一個derived class對象轉換為base class對象;繼承base的所有成員,在derived class中都是private。
private繼承意味implemented-in-terms-of(根據某物實現出)。如果class D以private形式繼承class B,我們的用意是採用class B內已經具備的某些特性。private繼承純粹只是一種實現技術(這也是為什麼derived class中,base class成員都是private的:因為它們都只是實現枝節而已)。private繼承意味只有實現部分被繼承,介面部分應略去。D以private形式繼承B,意思是D對象是根據B對象實現而得。
private繼承意味is-implemented-terms-of(根據某物實現出),和**條款**38的複合意義相同。那麼如何在兩者之間取捨?答案是儘可能的複合,必要時才使用private繼承。例如,當protected成員或virtual函數牽扯進來的時候。還有一種激進情況,當空間方面厲害關係足以踢翻private繼承支柱時,這個稍後討論。
現在有個Widget class,我們想記錄每個成員函數調用次數,在運行期間周期性審查這份資訊。為了完成這項工作,需要用到定時器:
class Timer{public: explicit Timer(int tickFrequency); virtual void OnTick() const;//定時器滴答一次,此函數調用一次 ……};
上面是可以調整頻率的訂一起,每次滴答調用某個virtual函數,我們可以重新定義那個virtual函數,來取出Widget當時狀態。為了重新定義Timer內的virtual函數,Widget必須繼承Timer。因為Widget不是Timer,因此不適用public繼承。還有一個觀點支援不適用public,Widget對象調用onTick有點奇怪,會違反條款18:讓介面容易被正確使用,不容易被誤用。
class Widget: private Timer{private: virtual void onTick() const;//查看Widget的資料等操作 ……};
這個設計也可以通過複合實現
class Widget{private: class WidgetTimer: public Timer{ public: virtual void onTick() const; …… }; WidgetTimer timer; ……};
這個設計稍微複雜一點,涉及到了public繼承和複合,以及匯入一個新class。我們有理由來選擇這個複合版本,而不是private繼承版本。
第一,Widget可能會有衍生類別,但是我們可能會想阻止在衍生類別中重新定義onTick。如果是使用private繼承,上面的想法就不能實現,因為derived classes可以重新定義virtual函數(**條款**35)。如果採用複用方案,Widget的derived classes將無法採用WidgetTimer,自然也就無法繼承或重新定義它的virtual函數了。這個像Java中的final或C#中的sealed。
第二,採用複合方案,還可以降低編譯依存性。如果Widget繼承Timer,當Widget編譯時間Timer的定義必須課件,所以Widget所在的定義檔案必須包含Timer的定義檔案。複合方案可以將WidgetTimer移出Widget,而只含有一個指標即可。
private繼承主要用於“當一個意欲成為derived class者想訪問一個意欲成為base class者的protected成分,或為了重新定義一個或多個virtual函數”。這時候,兩個classes之間關係是is-implemented-in-terms-of,而不是is-a。有一種激進情況涉及空間最佳化,會促使你選擇private繼承,而不是繼承加複合。
這個情況真夠激進,只適用於你所處理的class不帶任何資料。它不包含non-static變數、virtual函數,沒有繼承virtual base class。這樣的empty classes對象沒使用任何空間,因為它沒有任何資料對象要儲存。但是因為技術原因,C++對象都必須有非零大小
class Empty{};class HoldsAnInt{private: int x; Empty e;};
sizeof(HoldsAnInt)>sizeof(int)。大多數編譯器中,sizeof(Empty)為1,通常C++官網勒令安插一個char到對象內,但class大小還有位元組對其需求。
“獨立(非附屬)”對象大小一定不為零,這個約束不適用於derived class對象內的base成分,因為它們不獨立,如果繼承Empty,而不是複合
class HoldsAnInt: private Empty{private: int x;};
這時,幾乎可以確定sizeof(HoldsAnInt)==sizeof(int)。這是所謂的EBO(empty base optimization;空白基類最佳化)。如果客戶非常在意空間,那麼使用EBO。EBO一般只在單一繼承下才行,統治C++物件版面配置的那些規則通常表示EBO無法被施行餘“擁有多個base”的derived classes身上。
empty class並不是真的empty。它們內往往含有typedef、enum、static或弄-virtual函數。SLT有許多技術用途的empty classes,其中內含有的成員(通常是typedefs),包括base classes unary_function和binary_function,這些是“使用者自訂之函數對象”,通常會繼承的classes。
前面提到,只要可以儘可能選擇複合,但這也不是全部。當面對並不存在is-a關係的兩個classes,其中一個需要訪問另一個的protected成員,或需要重新定義其一個或多個virtual函數,private繼承可能成為正統設計策略。在考慮了其他方案後,仍然認為private繼承是“表現兩個classes之間的關係”的最佳辦法,那就使用它。
總結
- private繼承意味著is-implemented-in-terms-of(根據某物實現出)。它通常比複合(composition)的層級低。但是當derived class需要訪問protected base class的成員,或需要重新定義繼承而來的virtual函數時,使用private是合理的。
- 和複合(composition)不同,private繼承可以造成empty base最佳化。這對致力於“對象佔用空間最小化”的程式庫開發人員而言,可能很重要。
《Effective C++》:條款38-條款39