《Effective C++ 中文版 第三版》讀書(shū)筆記
** 條款 40:明智而審慎地使用多重繼承 **
一旦涉及多重繼承 (multiple inheritance涎跨;MI):
程序有可能從一個(gè)以上的 base class 繼承相同名稱(如函數(shù)、typedef 等)窿侈。那會(huì)導(dǎo)致較多的歧義機(jī)會(huì)泪勒。例如:
class BorrowableItem {
public:
void checkOut();
};
class ElectronicGadet {
private:
bool checkOut() const;
};
class MP3Player: public BorrowableItem
public ElectronicGadet
{...};
MP3Player mp;
mp.checkOut();//歧義田藐,調(diào)用的是哪個(gè)checkOut潜秋?
即使兩個(gè)之中只有一個(gè)可取用(ElectronicGadet 是 private)恨溜。這與 C++ 用來(lái)解析重載函數(shù)調(diào)用的規(guī)則相符:在看到是否有個(gè)函數(shù)可取之前垃杖,C++ 首先確認(rèn)這個(gè)函數(shù)對(duì)此調(diào)用之言是最佳匹配男杈。找出最佳匹配才檢驗(yàn)其可取用性。本例的兩個(gè) checkOut 有相同的匹配程度缩滨。沒(méi)有所謂最佳匹配势就。因此 ElectronicGadget::checkOut 的可取用性就從未被編譯器審查。
為了解決這個(gè)歧義脉漏,必須明白指出你要調(diào)用哪個(gè) base class 內(nèi)的函數(shù):
mp.BorrowableItem::checkOut();
你當(dāng)然也可以明確調(diào)用 ElectronicGadget::checkOut()苞冯,但然后你會(huì)獲得一個(gè) “嘗試調(diào)用 private 成員函數(shù)” 的錯(cuò)誤。
當(dāng)即稱一個(gè)以上的 base classes侧巨,這些 base classes 并不常在繼承體系中有更高級(jí)的 base classes舅锄,因?yàn)槟菚?huì)導(dǎo)致要命的 “鉆石型多重繼承”:
class File{...};
class InputFile: public File {...};
class OutputFile: public File{...};
class IOFile: public InputFile,
public OutputFile
{...};
任何時(shí)候只要你的繼承體系中某個(gè) base class 和某個(gè) derived class 之間有一條以上的想通路線,你就必須面對(duì)這樣一個(gè)問(wèn)題:是否打算讓 base class 內(nèi)的成員經(jīng)由每一條路徑被復(fù)制司忱?假設(shè) File 有個(gè)成員變量 fileName皇忿,那么 IOFile 應(yīng)給有兩份 fileName 成員變量。但從另一個(gè)角度來(lái)說(shuō)坦仍,簡(jiǎn)單的邏輯告訴我們鳍烁,IOFile 對(duì)象只有一個(gè)文件名稱,所以他繼承自兩個(gè) base class 而來(lái)的 fileName 不能重復(fù)繁扎。
C++ 的缺省做法是執(zhí)行重復(fù)幔荒。如果那不是你要的糊闽,你必須令那個(gè)帶有此數(shù)據(jù)的 base class(也就是 File)成為一個(gè) virtual base class。必須令所有直接繼承自它的 classes 采用 “virtual 繼承”:
class File{...};
class InputFile: virtual public File {...};
class OutputFile: virtual public File{...};
class IOFile: public InputFile,
public OutputFile
{...};
C++ 標(biāo)準(zhǔn)程序庫(kù)內(nèi)含一個(gè)多重繼承體系爹梁,只不過(guò)其 class 是 class template: basic_ios右犹,basic_istream,basic_ostream 和 basic_iostream姚垃。
從正確行為來(lái)看念链,public 繼承應(yīng)該總是 virtual。如果這是唯一一個(gè)觀點(diǎn)积糯,規(guī)則很簡(jiǎn)單:任何時(shí)候當(dāng)你使用 public 繼承掂墓,請(qǐng)改用 virtual public 繼承。但是看成,正確性并不是唯一觀點(diǎn)梆暮。為避免繼承來(lái)的成員變量重復(fù),編譯器必須提供若干幕后戲法绍昂,其后果就是:使用 virtual 繼承的那些 classes 所產(chǎn)生的對(duì)象往往比使用 non-virtual 繼承的兄弟們體積大啦粹,訪問(wèn) virtual base classes 的成員變量時(shí),也比訪問(wèn) non-virtual base classes 成員變量速度慢窘游。
virtual 繼承的成本還包括其他:支配 “virtual base classes 初始化” 的規(guī)則比起 non-virtual base 的情況遠(yuǎn)為復(fù)雜和不直觀唠椭。virtual base 的初始化責(zé)任是由繼承體系中的最底層(most derived)class 負(fù)責(zé),1忍饰、class 若派生自 virtual base class 而需要初始化贪嫂,必須認(rèn)知其 virtual bases —— 不論那些 bases 距離多遠(yuǎn),2艾蓝、當(dāng)一個(gè)新的 derived class 加入繼承體系中力崇,它必須承擔(dān)起 virtual bases(不論直接或間接)的初始化工作。
我們對(duì) virtual 繼承的忠告:第一赢织,非必要不要使用 virtual bases亮靴。第二,如果必須使用 virtual bases于置,盡可能避免在其中放置數(shù)據(jù)茧吊。這樣你就不需擔(dān)心這些 classes 身上的初始化(和賦值)所帶來(lái)的詭異事情了。
下面看看這個(gè) C++ Interface class:
class IPerson{
public:
virtual ~IPerson();
virtual std::string name() const = 0;
virtual std::string birthDate() const =0;
};
//factory function,根據(jù)一個(gè)獨(dú)一無(wú)二的數(shù)據(jù)庫(kù)ID創(chuàng)建一個(gè)Person對(duì)象
std::tr1::shared_ptr<IPerson> makePerson(DatabaseID personIdentifier);
DatabaseID askUserForDatabaseID();
DatabaseID id(askUserForDatabaseID());
std::tr1::shared_ptr<IPerson> pp(makePerson(id));
假設(shè)一個(gè)派生自 IPerson 的具象 class CPerson八毯,它必須提供 “繼承自 Iperson” 的 pure virtual 函數(shù)的實(shí)現(xiàn)代碼搓侄。我們可以寫(xiě)出這些,但更好的是利用既有組件话速。例如有個(gè)既有的數(shù)據(jù)庫(kù)相關(guān) class讶踪,PersonInfo:
class PersonInfo{
public:
explicit PersonInfo(DatabaseID pid);
virtual ~PersonInfo();
virtual const char* theName()const;
virtual const char* theBirthDate() const;
private:
virtual const char* valueDelimOpen() const;
virtual const char* valueDelimClose() const;
};
PersonInfo 被設(shè)計(jì)用來(lái)協(xié)助以各種格式打印數(shù)據(jù)庫(kù)字段,每個(gè)字段值的起始點(diǎn)和結(jié)束點(diǎn)以特殊字符串為界泊交。默認(rèn)為 “[”,“]”乳讥,但并非人人都愛(ài)方括號(hào)筹麸,所以提供兩個(gè) virtual 函數(shù) valueDelimOpen 和 ValueDelimClose 語(yǔ)序 derived class 設(shè)定他們自己的頭尾界限符號(hào)。PersonInfo 成員函數(shù)將調(diào)用這些 virtual 函數(shù)雏婶,把適當(dāng)?shù)慕缦薹?hào)添加到它們的返回值上。PersonInfo::theName 的代碼看起來(lái)像這樣:
const char* PersonInfo::valueDelimOpen() const
{
return "[";//default
}
const char* PersonInfo::valueDelimClose() const
{
return "]";//default
}
const char* PersonInfo::theName() const
{
//保留緩沖區(qū)給返回值使用:static白指,自動(dòng)初始化為“全0”
static char value[Max_Formatted_Field_Value_Length];
//寫(xiě)入起始符號(hào)
std::strcpy(value, valueDelimOpen());
//將value內(nèi)的字符串附到這個(gè)對(duì)象的name成員變量中
//寫(xiě)入結(jié)尾符號(hào)
std::strcat(value, valueDelimClose());
return value;
}
所以 theName 返回的結(jié)果不僅僅取決于 PersonInfo 也取決于從 PersonInfo 派生下去的 classes留晚。
Cperson 和 personInfo 的關(guān)系是,PersonInfo 剛好有若干函數(shù)可幫助 Cperson 比較容易實(shí)現(xiàn)出來(lái)告嘲。因此它們的關(guān)系是 is-implemented-in-term-of错维。這種關(guān)系可以兩種技術(shù)實(shí)現(xiàn):復(fù)合和 private 繼承。一般復(fù)合必要受歡迎橄唬,本例之中 Cperson 要重新定義 valueDelimOpen 和 valueDelimClose赋焕,所以直接的解法是 private 繼承。
Cperson 還有必須實(shí)現(xiàn) Iperson 的接口仰楚,那得要 public 繼承才能完成隆判。這導(dǎo)致多重繼承的一個(gè)通情達(dá)理的應(yīng)用:將 “public 繼承自某接口” 和 “private 繼承自某實(shí)現(xiàn)” 結(jié)合在一起:
class Cperson: public IPerson, private PersonInfo{
public:
explicit Cperson(DatabaseID pid): PersonInfo(pid){}
virtual std::string name() const
{
return PersonInfo::theName();
}
virtual std::string birthDate() const
{
return PersonInfo::theBirthDate();
}
private:
const char* valueDelimOpen() const {return "";}
const char* valueDelimClose() const {return "";}
};
如果你唯一能提出的設(shè)計(jì)涉及多重繼承,你應(yīng)該再努力想一想 —— 幾乎可以說(shuō)一定會(huì)有某些方案讓單一繼承行的通僧界。然而有時(shí)候多繼承的確是完成任務(wù)最簡(jiǎn)潔侨嘀、最易維護(hù)、最合理的做法捂襟,就別害怕使用它咬腕。只是確定,的確在明智而審慎的情況下使用它葬荷。
請(qǐng)記渍枪病:
多重繼承比單一繼承復(fù)雜。它可能導(dǎo)致新的歧義性宠漩,以及對(duì) virtual 繼承的需求举反。
virtual 繼承會(huì)增加大小、速度扒吁、初始化(及賦值)復(fù)雜度等等成本照筑。如果 virtual base class 不帶任何數(shù)據(jù),將是最具實(shí)用價(jià)值的情況瘦陈。
多重繼承的確有正當(dāng)用途凝危。其中一個(gè)情節(jié)涉及 “public 繼承某個(gè) Interface class” 和 “private 繼承某個(gè)協(xié)助實(shí)現(xiàn)的 class” 的兩相組合。