《Effective C++ 中文版 第三版》讀書(shū)筆記
** 條款 39:明智而審慎地使用 private 繼承 **
C++ 中 public 繼承視為 is-a 關(guān)系。現(xiàn)在看 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); // 錯(cuò)誤第晰! 難道學(xué)生不是人棵里?过蹂!
顯然 private 繼承不是 is-a 關(guān)系族檬。
由 private base class 繼承而來(lái)的所有成員茬腿,在 derived class 中都會(huì)成為 private 屬性呼奢,縱使它們?cè)?base class 中原本是 protected 或 public。private 繼承意味著 implemented-in-term-of(根據(jù)某物實(shí)現(xiàn))切平。
如果 class D 以 private 形式繼承 class B握础,用意是為了采用 class B 內(nèi)已經(jīng)備妥的某些特性,不是因?yàn)?B 對(duì)象和 D 對(duì)象存在任何觀念上的關(guān)系悴品。private 繼承純粹只是一種實(shí)現(xiàn)技術(shù)(這就是為什么繼承自 private base class 的每樣?xùn)|西在你的 class 內(nèi)都是 private:因?yàn)樗麄兌际菍?shí)現(xiàn)細(xì)節(jié)而已)禀综。用條款 34 的術(shù)語(yǔ)說(shuō),private 繼承意味著只有實(shí)現(xiàn)部分被繼承苔严,接口部分應(yīng)略去定枷。如果 D 以 private 形式繼承 B,意思是 D 對(duì)象根據(jù) B 對(duì)象實(shí)現(xiàn)而得届氢。private 繼承在軟件 “設(shè)計(jì)” 層面上沒(méi)有意義欠窒,其意義只及于軟件實(shí)現(xiàn)層面。
條款 38 說(shuō)復(fù)合(composition)的意義也是 is-implemented-in-term-of退子,如何進(jìn)行取舍岖妄?盡可能的使用復(fù)合型将,必要時(shí)才使用 private 繼承:主要是當(dāng)一個(gè)意欲成為 derived class 者想訪(fǎng)問(wèn)一個(gè)意欲成為 base class 者的 protected 成分,或?yàn)榱酥匦露x virtual 函數(shù)荐虐,還有一種激進(jìn)情況是空間方面的厲害關(guān)系七兜。
有個(gè) Widget class,它記錄每個(gè)成員函數(shù)的被調(diào)用次數(shù)福扬。運(yùn)行期周期性的審查那份信息腕铸。為了完成這項(xiàng)工作,我們需要設(shè)定某種定時(shí)器忧换,使我們知道收集統(tǒng)計(jì)數(shù)據(jù)的時(shí)候是否到了恬惯。
為了復(fù)用既有代碼,我們發(fā)現(xiàn)了 Timer:
class Timer {
public:
explicit Timer(int tickFrequency);
virtual void onTick() const;
};
每次滴答就調(diào)用某個(gè) virtual 函數(shù)亚茬,我們可以重新定義那個(gè) virtual 函數(shù),讓后者取出 Widget 的當(dāng)時(shí)狀態(tài)浓恳。
為了讓 Widget 重新定義 Timer 內(nèi)的 virtual 函數(shù)刹缝,Widget 必須繼承自 Timer。但 public 繼承并不適當(dāng)颈将,因?yàn)?Widget 并不是一個(gè) Timer梢夯。不能夠?qū)σ粋€(gè) Widget 調(diào)用 onTick 吧,觀念上那并不是 Wigdet 接口的一部分晴圾。
我們必須用 private 繼承 Timer:
class Widget: private Timer{
private:
virtual void onTick() const;
};
再說(shuō)一次颂砸,把 onTick 放進(jìn) public 接口內(nèi)會(huì)導(dǎo)致客戶(hù)以為他們可以調(diào)用它,那就違反了條款 18.
這個(gè)設(shè)計(jì)好死姚,但不值幾文錢(qián)人乓,private 繼承并非絕對(duì)必要。如果我們決定用復(fù)合取而代之都毒,是可以的色罚,只要在 Widget 內(nèi)聲明一個(gè)嵌套式 private class,后者以 public 形式繼承 Timer 并重新定義 onTick账劲,然后放一個(gè)這種類(lèi)型的對(duì)象在 Widget 內(nèi):
class Widget{
private:
class WidgetTimer: public Timer{
public:
virtual void onTick() const;
};
WidgetTimer timer;
};
這個(gè)設(shè)計(jì)比只是用 private 繼承復(fù)雜一些戳护,但是有兩個(gè)理由可能你愿意或應(yīng)該選擇這樣的 public 繼承加復(fù)合:
首先,或許會(huì)想設(shè)計(jì) Widget 使它得以擁有 derived classes瀑焦,但同時(shí)你可能會(huì)想阻止 derived clssses 重新定義 onTick腌且。如果 Widget 繼承自 Timer,上面的想法就不可能實(shí)現(xiàn)榛瓮,即使是 private 繼承也不可能铺董。(條款 35 說(shuō) derived class 可以重新定義 virtual 函數(shù),即使它們不得調(diào)用它)但如果 WidgetTimer 是 Widget 內(nèi)部的一個(gè) private 成員并繼承 Timer榆芦,Widget 的 derived classes 將無(wú)法取用 WidgetTimer柄粹,因此無(wú)法繼承它或重新定義它的 virtual 函數(shù)喘鸟。有些類(lèi)似 java 的 final 或 C# 的 sealed。
第二驻右,或許想要將 Widget 的編譯依存性降至最低什黑,若 Widget 繼承 Timer,當(dāng) Widget 被編譯時(shí)堪夭,timer 的定義必須可見(jiàn)愕把,所以定義 Widget 的那個(gè)文件必須包含 Timer.h。但如果 WidgetTimer 移除 Widget 之外而 Widget 內(nèi)含一個(gè)指針指向 WidgetTimer森爽,Widget 可以只帶著一個(gè)簡(jiǎn)單的 WidgetTimer 聲明式恨豁,不再需要 #include 任何與 timer 有關(guān)的東西。對(duì)大型系統(tǒng)而言爬迟,如此的解耦(decouplings)可能是重要的措施橘蜜。
還有一種激進(jìn)情況,只適用于你所處理的 class 不帶任何數(shù)據(jù)時(shí)付呕。這樣的 classes 沒(méi)有 non-static 成員變量计福,沒(méi)有 virtual 函數(shù)(這種函數(shù)會(huì)為每個(gè)對(duì)象帶來(lái)一個(gè) vptr),也沒(méi)有 virtual base classes(這樣的 base classes 也會(huì)招致體積上的額外開(kāi)銷(xiāo)徽职,見(jiàn)條款 40)象颖。這種所謂的 empty class 對(duì)象不使用任何空間,因?yàn)闆](méi)有任何隸屬對(duì)象的數(shù)據(jù)要存儲(chǔ)姆钉,然而由于技術(shù)上的理由说订,C++ 裁定凡是獨(dú)立(非附屬)對(duì)象都必須有非零大小:
class Empty{};
class HoldsAnInt {
private:
int x;
Empty e; // 應(yīng)該不需要任何內(nèi)存
};
你會(huì)發(fā)現(xiàn) sizeof(HoldsAnInt) > sizeof(int)潮瓶;一個(gè) Empty 成員變量竟然要求內(nèi)存陶冷。在多數(shù)編譯器中 sizeof(Empty) 獲得 1,因?yàn)槊鎸?duì) “大小為零的獨(dú)立對(duì)象”筋讨,通常 C++ 官方勒令默默安插一個(gè) char 到空對(duì)象內(nèi)埃叭。然而齊位需求(alignment)可能造成編譯器為類(lèi)似 HoldsAnInt 這樣的 class 加上一些襯墊(padding),所以有可能 HoldsAnInt 對(duì)象不只多一個(gè) char 大小悉罕,實(shí)際上放大到多一個(gè) int赤屋。
獨(dú)立(非附屬)這個(gè)約束不適用于 derived class 對(duì)象內(nèi)的 base class 成分,因?yàn)樗鼈儾⒎仟?dú)立壁袄。如果你繼承 Empty类早,而不是內(nèi)含一個(gè)那種類(lèi)型的對(duì)象:
class HoldsAnInt: private Empty{
private:
int x;
};
幾乎可以確定 sizeof(HoldsAnInt) == sizeof(int)。這是所謂的 EBO(empty base optimization:空白基類(lèi)最優(yōu)化)嗜逻,如果你是一個(gè)庫(kù)開(kāi)發(fā)成員涩僻,而你的客戶(hù)非常在意空間,那么值得注意 EBO。另外一個(gè)值得知道的是逆日,一般 EBO 只在單一繼承(而非多繼承)下才可行嵌巷。
現(xiàn)實(shí)中的 “Empty” class 并不是真的 empty。雖然他們從未擁有 non-static 成員變量室抽,卻往往內(nèi)含 typedefs搪哪, enums, static 成員變量坪圾,或 non-virtual 函數(shù)晓折。stl 就有許多技術(shù)用途的 empty classes,其中內(nèi)含有用的成員(通常是 typedefs)兽泄,包括 base classes unary_function 和 binary_function漓概,這些是 “用戶(hù)自定義的函數(shù)對(duì)象” 通常都會(huì)繼承的 classes。感謝 EBO 的廣泛實(shí)踐病梢,這樣的繼承很少增加 derived classes 的大小胃珍。
盡管如此,大多數(shù) class 并非 empty蜓陌,所以 EBO 很少成為 private 繼承的正當(dāng)理由堂鲜。復(fù)合和 private 繼承都意味著 is-implemented-in-term-of,但復(fù)合比較容易理解护奈,所以無(wú)論什么時(shí)候,只要可以哥纫,還是應(yīng)該選擇復(fù)合霉旗。
請(qǐng)記住:
private 繼承意味 is-implementation-in-terms of(根據(jù)某物實(shí)現(xiàn)出)蛀骇。她通常比復(fù)合級(jí)別低厌秒。但是當(dāng) derived class 需要訪(fǎng)問(wèn) protected base class 的成員,或需要重新定義繼承而來(lái)的 virtual 函數(shù)時(shí)擅憔,這么設(shè)計(jì)是合理的鸵闪。
和復(fù)合不同,private 繼承可以造成 empty base 最優(yōu)化暑诸。這對(duì)致力于 “對(duì)象尺寸最小化” 的程序庫(kù)開(kāi)發(fā)者而言蚌讼,可能很重要。