委托(delegate)
和成員函數(shù)指針不同,你不難發(fā)現(xiàn)委托的用處。最重要的,使用委托可以很容易地實(shí)現(xiàn)一個(gè) Subject/Observer設(shè)計(jì)模式的改進(jìn)版[GoF, p. 293]。Observer(觀察者)模式顯然在GUI中有很多的應(yīng)用,但我發(fā)現(xiàn)它對(duì)應(yīng)用程序核心的設(shè)計(jì)也有很大的作用。委托也可用來(lái)實(shí)現(xiàn)策略(Strategy)[GoF, p. 315]和狀態(tài)(State)[GoF, p. 305]模式。
現(xiàn)在,我來(lái)說(shuō)明一個(gè)事實(shí),委托和成員函數(shù)指針相比并不僅僅是好用,而且比成員函數(shù)指針簡(jiǎn)單得多!既然所有的.NET語(yǔ)言都實(shí)現(xiàn)了委托,你可能會(huì)猜想如此高層的概念在匯編代碼中并不好實(shí)現(xiàn)。但事實(shí)并不是這樣:委托的實(shí)現(xiàn)確實(shí)是一個(gè)底層的概念,而且就像普通的函數(shù)調(diào)用一樣簡(jiǎn)單(并且很高效)。一個(gè)C++委托只需要包含一個(gè)this 指針和一個(gè)簡(jiǎn)單的函數(shù)指針就夠了。當(dāng)你建立一個(gè)委托時(shí),你提供這個(gè)委托一個(gè)this指針,并向它指明需要調(diào)用哪一個(gè)函數(shù)。編譯器可以在建立委托時(shí)計(jì)算出調(diào)整this指針需要的偏移量。這樣在使用委托的時(shí)候,編譯器就什么事情都不用做了。這一點(diǎn)更好的是,編譯器可以在編譯時(shí)就可以完成全部這些工作,這樣的話,委托的處理對(duì)編譯器來(lái)說(shuō)可以說(shuō)是微不足道的工作了。在x86系統(tǒng)下將委托處理成的匯編代碼就應(yīng)該是這么簡(jiǎn)單:
mov ecx, [this]
call [pfunc]
但是,在標(biāo)準(zhǔn)C++中卻不能生成如此高效的代碼。 Borland為了解決委托的問(wèn)題在它的C++編譯器中加入了一個(gè)新的關(guān)鍵字(__closure),用來(lái)通過(guò)簡(jiǎn)潔的語(yǔ)法生成優(yōu)化的代碼。GNU編譯器也對(duì)語(yǔ)言進(jìn)行了擴(kuò)展,但和Borland的編譯器不兼容。如果你使用了這兩種語(yǔ)言擴(kuò)展中的一種,你就會(huì)限制自己只使用一個(gè)廠家的編譯器。而如果你仍然遵循標(biāo)準(zhǔn)C++的規(guī)則,你仍然可以實(shí)現(xiàn)委托,但實(shí)現(xiàn)的委托就不會(huì)是那么高效了。
有趣的是,在C#和其他.NET語(yǔ)言中,執(zhí)行一個(gè)委托的時(shí)間要比一個(gè)函數(shù)調(diào)用慢8倍(參見(jiàn)http://msdn.microsoft.com/library/en- us/dndotnet/html/fastmanagedcode.asp)。我猜測(cè)這可能是垃圾收集和.NET安全檢查的需要。最近,微軟將“統(tǒng)一事件模型(unified event model)”加入到Visual C++中,隨著這個(gè)模型的加入,增加了__event、 __raise、__hook、__unhook、event_source和event_receiver等一些關(guān)鍵字。坦白地說(shuō),我對(duì)加入的這些特性很反感,因?yàn)檫@是完全不符合標(biāo)準(zhǔn)的,這些語(yǔ)法是丑陋的,因?yàn)樗鼈兪惯@種C++不像C++,并且會(huì)生成一堆執(zhí)行效率極低的代碼。
解決這個(gè)問(wèn)題的推動(dòng)力:對(duì)高效委托(fast delegate)的迫切需求
使用標(biāo)準(zhǔn)C++實(shí)現(xiàn)委托有一個(gè)過(guò)度臃腫的癥狀。大多數(shù)的實(shí)現(xiàn)方法使用的是同一種思路。這些方法的基本觀點(diǎn)是將成員函數(shù)指針看成委托??但這樣的指針只能被一個(gè)單獨(dú)的類使用。為了避免這種局限,你需要間接地使用另一種思路:你可以使用模版為每一個(gè)類建立一個(gè)“成員函數(shù)調(diào)用器(member function invoker)”。委托包含了this指針和一個(gè)指向調(diào)用器(invoker)的指針,并且需要在堆上為成員函數(shù)調(diào)用器分配空間。
對(duì)于這種方案已經(jīng)有很多種實(shí)現(xiàn),包括在CodeProject上的實(shí)現(xiàn)方案。各種實(shí)現(xiàn)在復(fù)雜性上、語(yǔ)法(比如,有的和C#的語(yǔ)法很接近)上、一般性上有所不同。最具權(quán)威的一個(gè)實(shí)現(xiàn)是boost::function。最近,它已經(jīng)被采用作為下一個(gè)發(fā)布的C++標(biāo)準(zhǔn)版本中的一部分[Sutter1]。希望它能夠被廣泛地使用。
就像傳統(tǒng)的委托實(shí)現(xiàn)方法一樣,我同樣發(fā)覺(jué)這種方法并不十分另人滿意。雖然它提供了大家所期望的功能,但是會(huì)混淆一個(gè)潛在的問(wèn)題:人們?nèi)狈?duì)一個(gè)語(yǔ)言的底層的構(gòu)造。 “成員函數(shù)調(diào)用器”的代碼對(duì)幾乎所有的類都是一樣的,在所有平臺(tái)上都出現(xiàn)這種情況是令人沮喪的。畢竟,堆被用上了。但在一些應(yīng)用場(chǎng)合下,這種新的方法仍然無(wú)法被接受。
我做的一個(gè)項(xiàng)目是離散事件模擬器,它的核心是一個(gè)事件調(diào)度程序,用來(lái)調(diào)用被模擬的對(duì)象的成員函數(shù)。大多數(shù)成員函數(shù)非常簡(jiǎn)單:它們只改變對(duì)象的內(nèi)部狀態(tài),有時(shí)在事件隊(duì)列(event queue)中添加將來(lái)要發(fā)生的事件,在這種情況下最適合使用委托。但是,每一個(gè)委托只被調(diào)用(invoked)一次。一開(kāi)始,我使用了boost:: function,但我發(fā)現(xiàn)程序運(yùn)行時(shí),給委托所分配的內(nèi)存空間占用了整個(gè)程序空間的三分之一還要多!“我要真正的委托!”我在內(nèi)心呼喊著,“真正的委托只需要僅僅兩行匯編指令啊!”
我并不能總是能夠得到我想要的,但后來(lái)我很幸運(yùn)。我在這兒展示的代碼(代碼下載鏈接見(jiàn)譯者注)幾乎在所有編譯環(huán)境中都產(chǎn)生了優(yōu)化的匯編代碼。最重要的是,調(diào)用一個(gè)含有單個(gè)目標(biāo)的委托(single-target delegate)的速度幾乎同調(diào)用一個(gè)普通函數(shù)一樣快。實(shí)現(xiàn)這樣的代碼并沒(méi)有用到什么高深的東西,唯一的遺憾就是,為了實(shí)現(xiàn)目標(biāo),我的代碼和標(biāo)準(zhǔn)C++ 的規(guī)則有些偏離。我使用了一些有關(guān)成員函數(shù)指針的未公開(kāi)知識(shí)才使它能夠這樣工作。如果你很細(xì)心,而且不在意在少數(shù)情況下的一些編譯器相關(guān)(compiler-specific)的代碼,那么高性能的委托機(jī)制在任何C++編譯器下都是可行的。
訣竅:將任何類型的成員函數(shù)指針轉(zhuǎn)化為一個(gè)標(biāo)準(zhǔn)的形式
我的代碼的核心是一個(gè)能夠?qū)⑷魏晤惖闹羔樅腿魏纬蓡T函數(shù)指針?lè)謩e轉(zhuǎn)換為一個(gè)通用類的指針和一個(gè)通用成員函數(shù)的指針的類。由于C++沒(méi)有“通用成員函數(shù)(geneic member function)”的類型,所以我把所有類型的成員函數(shù)都轉(zhuǎn)化為一個(gè)在代碼中未定義的CGenericClass類的成員函數(shù)。
大多數(shù)編譯器對(duì)所有的成員函數(shù)指針平等地對(duì)待,不管他們屬于哪個(gè)類。所以對(duì)這些編譯器來(lái)說(shuō),可以使用reinterpret_cast將一個(gè)特定的成員函數(shù)指針轉(zhuǎn)化為一個(gè)通用成員函數(shù)指針。事實(shí)上,假如編譯器不可以,那么這個(gè)編譯器是不符合標(biāo)準(zhǔn)的。對(duì)于一些接近標(biāo)準(zhǔn)(almost-compliant)的編譯器,比如Digital Mars,成員函數(shù)指針的reinterpret_cast轉(zhuǎn)換一般會(huì)涉及到一些額外的特殊代碼,當(dāng)進(jìn)行轉(zhuǎn)化的成員函數(shù)的類之間沒(méi)有任何關(guān)聯(lián)時(shí),編譯器會(huì)出錯(cuò)。對(duì)這些編譯器,我們使用一個(gè)名為horrible_cast的內(nèi)聯(lián)函數(shù)(在函數(shù)中使用了一個(gè)union來(lái)避免C++的類型檢查)。使用這種方法看來(lái)是不可避免的??boost::function也用到了這種方法。
對(duì)于其他的一些編譯器(如Visual C++, Intel C++和Borland C++),我們必須將多重(multiple-)繼承和虛擬(virtual-)繼承類的成員函數(shù)指針轉(zhuǎn)化為單一(single-)繼承類的函數(shù)指針。為了實(shí)現(xiàn)這個(gè)目的,我巧妙地使用了模板并利用了一個(gè)奇妙的戲法。注意,這個(gè)戲法的使用是因?yàn)檫@些編譯器并不是完全符合標(biāo)準(zhǔn)的,但是使用這個(gè)戲法得到了回報(bào):它使這些編譯器產(chǎn)生了優(yōu)化的代碼。
既然我們知道編譯器是怎樣在內(nèi)部存儲(chǔ)成員函數(shù)指針的,并且我們知道在問(wèn)題中應(yīng)該怎樣為成員函數(shù)指針調(diào)整this指針,我們的代碼在設(shè)置委托時(shí)可以自己調(diào)整this指針。對(duì)單一繼承類的函數(shù)指針,則不需要進(jìn)行調(diào)整;對(duì)多重繼承,則只需要一次加法就可完成調(diào)整;對(duì)虛擬繼承...就有些麻煩了。但是這樣做是管用的,并且在大多數(shù)情況下,所有的工作都在編譯時(shí)完成!
這是最后一個(gè)訣竅。我們?cè)鯓訁^(qū)分不同的繼承類型?并沒(méi)有官方的方法來(lái)讓我們區(qū)分一個(gè)類是多重繼承的還是其他類型的繼承。但是有一種巧妙的方法,你可以查看我在前面給出了一個(gè)列表(見(jiàn)中篇)——對(duì)MSVC,每種繼承方式產(chǎn)生的成員函數(shù)指針的大小是不同的。所以,我們可以基于成員函數(shù)指針的大小使用模版!比如對(duì)多重繼承類型來(lái)說(shuō),這只是個(gè)簡(jiǎn)單的計(jì)算。而在確定unknown_inheritance(16字節(jié))類型的時(shí)候,也會(huì)采用類似的計(jì)算方法。
對(duì)于微軟和英特爾的編譯器中采用不標(biāo)準(zhǔn)12字節(jié)的虛擬繼承類型的指針的情況,我引發(fā)了一個(gè)編譯時(shí)錯(cuò)誤(compile-time error),因?yàn)樾枰粋€(gè)特定的運(yùn)行環(huán)境(workaround)。如果你在MSVC中使用虛擬繼承,要在聲明類之前使用 FASTDELEGATEDECLARE宏。而這個(gè)類必須使用unknown_inheritance(未知繼承類型)指針(這相當(dāng)于一個(gè)假定的 __unknown_inheritance關(guān)鍵字)。例如:
FASTDELEGATEDECLARE(CDerivedClass)
class CDerivedClass : virtual public CBaseClass1, virtual public CBaseClass2 {
// : (etc)
};
這個(gè)宏和一些常數(shù)的聲明是在一個(gè)隱藏的命名空間中實(shí)現(xiàn)的,這樣在其他編譯器中使用時(shí)也是安全的。MSVC(7.0或更新版本)的另一種方法是在工程中使用/vmg編譯器選項(xiàng)。而Inter的編譯器對(duì)/vmg編譯器選項(xiàng)不起作用,所以你必須在虛擬繼承類中使用宏。我的這個(gè)代碼是因?yàn)榫幾g器的bug才可以正確運(yùn)行,你可以查看代碼來(lái)了解更多細(xì)節(jié)。而在遵從標(biāo)準(zhǔn)的編譯器中不需要注意這么多,況且在任何情況下都不會(huì)妨礙FASTDELEGATEDECLARE宏的使用。
一旦你將類的對(duì)象指針和成員函數(shù)指針轉(zhuǎn)化為標(biāo)準(zhǔn)形式,實(shí)現(xiàn)單一目標(biāo)的委托(single-target delegate)就比較容易了(雖然做起來(lái)感覺(jué)冗長(zhǎng)乏味)。你只要為每一種具有不同參數(shù)的函數(shù)制作相應(yīng)的模板類就行了。實(shí)現(xiàn)其他類型的委托的代碼也大都與此相似,只是對(duì)參數(shù)稍做修改罷了。
這種用非標(biāo)準(zhǔn)方式轉(zhuǎn)換實(shí)現(xiàn)的委托還有一個(gè)好處,就是委托對(duì)象之間可以用等式比較。目前實(shí)現(xiàn)的大多數(shù)委托無(wú)法做到這一點(diǎn),這使這些委托不能勝任一些特定的任務(wù),比如實(shí)現(xiàn)多播委托(multi-cast delegates) [Sutter3]。
靜態(tài)函數(shù)作為委托目標(biāo)(delegate target)
理論上,一個(gè)簡(jiǎn)單的非成員函數(shù)(non-member function),或者一個(gè)靜態(tài)成員函數(shù)(static member function)可以被作為委托目標(biāo)(delegate target)。這可以通過(guò)將靜態(tài)函數(shù)轉(zhuǎn)換為一個(gè)成員函數(shù)來(lái)實(shí)現(xiàn)。我有兩種方法實(shí)現(xiàn)這一點(diǎn),兩種方法都是通過(guò)使委托指向調(diào)用這個(gè)靜態(tài)函數(shù)的“調(diào)用器(invoker)”的成員函數(shù)的方法來(lái)實(shí)現(xiàn)的。