別 面向?qū)ο蠹夹g(shù)最早出現(xiàn)于1960年代的Simula 67系統(tǒng),并且在1970年代保羅阿托實驗室開發(fā)的Smalltalk系統(tǒng)中發(fā)展成熟。然而對于大部分程序員來說,C++是第一個可用的面向?qū)ο蟪绦蛟O(shè)計語言。因此,我們關(guān)于面向?qū)ο蟮暮芏喔拍詈退枷胫苯觼碜杂贑++。但是,C++在實現(xiàn)面向?qū)ο笾嘘P(guān)鍵的多態(tài)性時,選擇了與Smalltalk完全不同的方案。其結(jié)果是,盡管在表面上兩者都實現(xiàn)了相似的多態(tài)性,但是在實踐中卻有著巨大的區(qū)別。具體的說,C++的多態(tài)性實現(xiàn)更加高效,但是并不適用于所有場合。很多經(jīng)驗不足的C++開發(fā)者不明白這個道理,在不合適的場合強行使用C++的多態(tài)性機制,落入削足適履的陷阱而不能自拔。本文將詳細探討C++多態(tài)性技術(shù)的局限性及解決的辦法。
兩種不同虛方法調(diào)用實現(xiàn)技術(shù)
C++的多態(tài)性是C++實現(xiàn)面向?qū)ο蠹夹g(shù)的基礎(chǔ)。具體的說,通過一個指向基類的指針調(diào)用虛成員函數(shù)的時候,運行時系統(tǒng)將能夠根據(jù)指針所指向的實際對象調(diào)用恰當?shù)某蓡T函數(shù)實現(xiàn)。如下所示:
class Base {
public:
virtual void vmf() { ... }
};
class Derived : public Base {
public:
virtual void vmf() { ... }
};
Base* p = new Base();
p->vmf(); // 這里調(diào)用Base::vmf
p = new Derived();
p->vmf(); // 這里調(diào)用
// Derived::vmf
...
請注意代碼中突出注釋的兩行,雖然其表面語法完全相同,但是卻分別調(diào)用了不同的函數(shù)實現(xiàn)。所謂的“多態(tài)”即就此而言。這些知識是每一個C++開發(fā)者都熟知的。
現(xiàn)在我們假設(shè)自己是語言的實現(xiàn)者,我們應(yīng)當如何來實現(xiàn)這種多態(tài)性?稍加思考,我們不難得到一個基本的思路。多態(tài)性的實現(xiàn)要求我們增加一個間接層,在這個間接層中攔截對于方法的調(diào)用,然后根據(jù)指針所指向的實際對象調(diào)用相應(yīng)的方法實現(xiàn)。在這個過程中我們?nèi)藶?BR> 增加的這個間接層非常重要,它需要完成以下幾項工作:
1. 獲知方法調(diào)用的全部信息,包括被調(diào)用的是哪個方法,傳入的實際參數(shù)有哪些。
2. 獲知調(diào)用發(fā)生時指針(引用)所指向的實際對象。
3. 根據(jù)第1、2步獲得的信息,找到合適的方法實現(xiàn)代碼,執(zhí)行調(diào)用。
這里的關(guān)鍵在于如何在第3 步中找到合適的方法實現(xiàn)代碼。由于多態(tài)性是就對象而言的,因此我們在設(shè)計時要把合適的方法實現(xiàn)代碼與對象綁定到一起。也就是說,必須在對象級別實現(xiàn)一個查找表結(jié)構(gòu),根據(jù)1、2步獲得的對象和方法信息,在這個查找表中找到實際的方法代碼地址,并加以調(diào)用?,F(xiàn)在問題變成了,我們應(yīng)當根據(jù)什么信息進行方法查找。對于這個問題有兩個不同的解決思路,一個是根據(jù)名稱進行查找,另一個是根據(jù)位置進行查找。粗看上去這兩種思路似乎沒什么大的差別,但是在實踐中,這兩種不同的實現(xiàn)思路導(dǎo)致了巨大的差別。下面我們詳細地加以考察。
在Smalltalk、Python、Ruby等動態(tài)面向?qū)ο笳Z言中,實際方法的查找是根據(jù)方法名稱進行的,其查找表結(jié)構(gòu)如下:
由于這種查找表根據(jù)方法的名稱進行方法查找,因此在查找過程中涉及字符串比較,效率較差。但是這種查找表有一個突出的優(yōu)點,就是有效空間利用率高。為了說明這一點,我們假設(shè)一個基類Base中有100個方法可供派生類改寫(因此所有Base對象所共享的方法查找表有100項),而它的一個派生類Derived僅僅只打算改寫其中5個方法,那么Derived類對象的方法查找表只需要5項。當一個方法調(diào)用發(fā)生的時候,runtime根據(jù)被調(diào)用的方法名稱在這個長度為5 的方法查找表中進行字符串查找,如果發(fā)現(xiàn)該方法在查找表中,則執(zhí)行調(diào)用,否則將調(diào)用轉(zhuǎn)寄(forward)給Base類執(zhí)行。這是虛方法調(diào)用的標準行為。當派生類實際改寫的方法數(shù)量很少的時候,可以將查找表安排成線性表,查找時順序比較,這種情況下有效空間利用率達到100%。如果派生類實際改寫的方法數(shù)量較多,那么可以采用散列表,如果采用合理的散列函數(shù),同樣可以在空間利用率很高(一般可接近75%).. 的情況下實現(xiàn)方法的快速查找。應(yīng)當注意到,由于編譯器可以很容易地獲得所有被改寫方法的名稱,因此可以執(zhí)行標準的gperf算法獲得的散列函數(shù)。
事實上,我們還可以這樣理解這種方案的優(yōu)勢,把表中每一項的“方法名”項視為“方法地址”項的描述信息,因此可以認為這種方案中的方法查找表攜帶自描述信息(或者稱為元數(shù)據(jù))?;谶@種攜帶自描述信息的數(shù)據(jù)結(jié)構(gòu),可以實現(xiàn)豐富多彩的擴展功能,比如在運行時
插入新的方法,或者用戶層次上的方法調(diào)用截獲等。因此,我們可以說這一方案的適用面廣,強大靈活,但在執(zhí)行效率上并非
兩種不同虛方法調(diào)用實現(xiàn)技術(shù)
C++的多態(tài)性是C++實現(xiàn)面向?qū)ο蠹夹g(shù)的基礎(chǔ)。具體的說,通過一個指向基類的指針調(diào)用虛成員函數(shù)的時候,運行時系統(tǒng)將能夠根據(jù)指針所指向的實際對象調(diào)用恰當?shù)某蓡T函數(shù)實現(xiàn)。如下所示:
class Base {
public:
virtual void vmf() { ... }
};
class Derived : public Base {
public:
virtual void vmf() { ... }
};
Base* p = new Base();
p->vmf(); // 這里調(diào)用Base::vmf
p = new Derived();
p->vmf(); // 這里調(diào)用
// Derived::vmf
...
請注意代碼中突出注釋的兩行,雖然其表面語法完全相同,但是卻分別調(diào)用了不同的函數(shù)實現(xiàn)。所謂的“多態(tài)”即就此而言。這些知識是每一個C++開發(fā)者都熟知的。
現(xiàn)在我們假設(shè)自己是語言的實現(xiàn)者,我們應(yīng)當如何來實現(xiàn)這種多態(tài)性?稍加思考,我們不難得到一個基本的思路。多態(tài)性的實現(xiàn)要求我們增加一個間接層,在這個間接層中攔截對于方法的調(diào)用,然后根據(jù)指針所指向的實際對象調(diào)用相應(yīng)的方法實現(xiàn)。在這個過程中我們?nèi)藶?BR> 增加的這個間接層非常重要,它需要完成以下幾項工作:
1. 獲知方法調(diào)用的全部信息,包括被調(diào)用的是哪個方法,傳入的實際參數(shù)有哪些。
2. 獲知調(diào)用發(fā)生時指針(引用)所指向的實際對象。
3. 根據(jù)第1、2步獲得的信息,找到合適的方法實現(xiàn)代碼,執(zhí)行調(diào)用。
這里的關(guān)鍵在于如何在第3 步中找到合適的方法實現(xiàn)代碼。由于多態(tài)性是就對象而言的,因此我們在設(shè)計時要把合適的方法實現(xiàn)代碼與對象綁定到一起。也就是說,必須在對象級別實現(xiàn)一個查找表結(jié)構(gòu),根據(jù)1、2步獲得的對象和方法信息,在這個查找表中找到實際的方法代碼地址,并加以調(diào)用?,F(xiàn)在問題變成了,我們應(yīng)當根據(jù)什么信息進行方法查找。對于這個問題有兩個不同的解決思路,一個是根據(jù)名稱進行查找,另一個是根據(jù)位置進行查找。粗看上去這兩種思路似乎沒什么大的差別,但是在實踐中,這兩種不同的實現(xiàn)思路導(dǎo)致了巨大的差別。下面我們詳細地加以考察。
在Smalltalk、Python、Ruby等動態(tài)面向?qū)ο笳Z言中,實際方法的查找是根據(jù)方法名稱進行的,其查找表結(jié)構(gòu)如下:
由于這種查找表根據(jù)方法的名稱進行方法查找,因此在查找過程中涉及字符串比較,效率較差。但是這種查找表有一個突出的優(yōu)點,就是有效空間利用率高。為了說明這一點,我們假設(shè)一個基類Base中有100個方法可供派生類改寫(因此所有Base對象所共享的方法查找表有100項),而它的一個派生類Derived僅僅只打算改寫其中5個方法,那么Derived類對象的方法查找表只需要5項。當一個方法調(diào)用發(fā)生的時候,runtime根據(jù)被調(diào)用的方法名稱在這個長度為5 的方法查找表中進行字符串查找,如果發(fā)現(xiàn)該方法在查找表中,則執(zhí)行調(diào)用,否則將調(diào)用轉(zhuǎn)寄(forward)給Base類執(zhí)行。這是虛方法調(diào)用的標準行為。當派生類實際改寫的方法數(shù)量很少的時候,可以將查找表安排成線性表,查找時順序比較,這種情況下有效空間利用率達到100%。如果派生類實際改寫的方法數(shù)量較多,那么可以采用散列表,如果采用合理的散列函數(shù),同樣可以在空間利用率很高(一般可接近75%).. 的情況下實現(xiàn)方法的快速查找。應(yīng)當注意到,由于編譯器可以很容易地獲得所有被改寫方法的名稱,因此可以執(zhí)行標準的gperf算法獲得的散列函數(shù)。
事實上,我們還可以這樣理解這種方案的優(yōu)勢,把表中每一項的“方法名”項視為“方法地址”項的描述信息,因此可以認為這種方案中的方法查找表攜帶自描述信息(或者稱為元數(shù)據(jù))?;谶@種攜帶自描述信息的數(shù)據(jù)結(jié)構(gòu),可以實現(xiàn)豐富多彩的擴展功能,比如在運行時
插入新的方法,或者用戶層次上的方法調(diào)用截獲等。因此,我們可以說這一方案的適用面廣,強大靈活,但在執(zhí)行效率上并非