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