成員函數指針??為什么那么復雜?
類的成員函數和標準的C函數有一些不同。與被顯式聲明的參數相似,類的成員函數有一個隱藏的參數this,它指向一個類的實例。根據不同的編譯器,this或者被看作內部的一個正常的參數,或者會被特別對待(比如,在VC++中,this一般通過ECX寄存器來傳遞,而普通的成員函數的參數被直接壓在堆棧中)。this作為參數和其他普通的參數有著本質的不同,即使一個成員函數受一個普通函數的支配,在標準C++中也沒有理由使這個成員函數和其他的普通函數(ordinary function)的行為相同,因為沒有thiscall關鍵字來保證它使用像普通參數一樣正常的調用規則。成員函數是一回事,普通函數是另外一回事(Member functions are from Mars, ordinary functions are from Venus)。
你可能會猜測,一個成員函數指針和一個普通函數指針一樣,只是一個代碼指針。然而這種猜測也許是錯誤的。在大多數編譯器中,一個成員函數指針要比一個普通的函數指針要大許多。更奇怪的是,在Visual C++中,一個成員函數指針可以是4、8、12甚至16個字節長,這取決于它所相關的類的性質,同時也取決于編譯器使用了怎樣的編譯設置!成員函數指針比你想象中的要復雜得多,但也不總是這樣。
讓我們回到二十世紀80年代初期,那時,最古老的C++編譯器CFront剛剛開發完成,那時C++語言只能實現單一繼承,而且成員函數指針剛被引入,它們很簡單:它們就像普通的函數指針,只是附加了額外的this作為它們的第一個參數,你可以將一個成員函數指針轉化成一個普通的函數指針,并使你能夠對這個額外添加的參數產生足夠的重視。
這個田園般的世界隨著CFront 2.0的問世被擊得粉碎。它引入了模版和多重繼承,多重繼承所帶來的破壞造成了成員函數指針的改變。問題在于,隨著多重繼承,調用之前你不知道使用哪一個父類的this指針,比如,你有4個類定義如下:
class A {
public:
virtual int Afunc() { return 2; };
};
class B {
public:
int Bfunc() { return 3; };
};
// C是個單一繼承類,它只繼承于A
class C: public A {
public:
int Cfunc() { return 4; };
};
// D 類使用了多重繼承
class D: public A, public B {
public:
int Dfunc() { return 5; };
};
假如我們建立了C類的一個成員函數指針。在這個例子中,Afunc和Cfunc都是C的成員函數,所以我們的成員函數指針可以指向Afunc或者Cfunc。但是Afunc需要一個this指針指向C::A(后面我叫它Athis),而Cfunc需要一個this指針指向C(后面我叫它Cthis)。編譯器的設計者們為了處理這種情況使用了一個把戲(trick):他們保證了A類在物理上保存在C類的頭部(即C類的起始地址也就是一個A類的一個實例的起始地址),這意味著Athis == Cthis。我們只需擔心一個this指針就夠了,并且對于目前這種情況,所有的問題處理得還可以。
現在,假如我們建立一個D類的成員函數指針。在這種情況下,我們的成員函數指針可以指向Afunc、Bfunc或Dfunc。但是Afunc需要一個this指針指向D::A,而Bfunc需要一個this指針指向D::B。這時,這個把戲就不管用了,我們不可以把A類和B類都放在D類的頭部。所以,D類的一個成員函數指針不僅要說明要指明調用的是哪一個函數,還要指明使用哪一個this指針。編譯器知道A類占用的空間有多大,所以它可以對Athis增加一個delta = sizeof(A)偏移量就可以將Athis指針轉換為Bthis指針。
如果你使用虛擬繼承(virtual inheritance),比如虛基類,情況會變得更糟,你可以不必為搞懂這是為什么太傷腦筋。就舉個例子來說吧,編譯器使用虛擬函數表(virtual function table??“vtable”)來保存每一個虛函數、函數的地址和virtual_delta:將當前的this指針轉換為實際函數需要的this指針時所要增加的位移量。
綜上所述,為了支持一般形式的成員函數指針,你需要至少三條信息:函數的地址,需要增加到this指針上的delta位移量,和一個虛擬函數表中的索引。對于MSVC來說,你需要第四條信息:虛擬函數表(vtable)的地址。
成員函數指針的實現
那么,編譯器是怎樣實現成員函數指針的呢?這里是對不同的32、64和16位的編譯器,對各種不同的數據類型(有int、void*數據指針、代碼指針(比如指向靜態函數的指針)、在單一(single-)繼承、多重(multiple-)繼承、虛擬(virtual-)繼承和未知類型(unknown)的繼承下的類的成員函數指針)使用sizeof運算符計算所獲得的數據:
編譯器
選項
int
DataPtr
CodePtr
Single
Multi
Virtual
Unknown
MSVC
4
4
4
4
8
12
16
MSVC
/vmg
4
4
4
16#
16#
16#
16
MSVC
/vmg /vmm
4
4
4
8#
8#
--
8#
Intel_IA32
4
4
4
4
8
12
12
Intel_IA32
/vmg /vmm
4
4
4
4
8
--
8
Intel_Itanium
4
8
8
8
12
20
20
G++
4
4
4
8
8
8
8
Comeau
4
4
4
8
8
8
8
DMC
4
4
4
4
4
4
4
BCC32
4
4
4
12
12
12
12
BCC32
/Vmd
4
4
4
4
8
12
12
WCL386
4
4
4
12
12
12
12
CodeWarrior
4
4
4
12
12
12
12
XLC
4
8
8
20
20
20
20
DMC
small
2
2
2
2
2
2
2
DMC
medium
2
2
4
4
4
4
4
WCL
small
2
2
2
6
6
6
6
WCL
compact
2
4
2
6
6
6
6
WCL
medium
2
2
4
8
8
8
8
WCL
large
2
4
4
8
8
8
8
注: #表示使用__single/__multi/__virtual_inheritance關鍵字的時候代表4、8或12。這些編譯器是Microsoft Visual C++ 4.0 to 7.1 (.NET 2003), GNU G++ 3.2 (MingW binaries, http://www.mingw.org/), Borland BCB 5.1 (http://www.borland.com/), Open Watcom (WCL) 1.2 (http://www.openwatcom.org/), Digital Mars (DMC) 8.38n (http://www.digitalmars.com/), Intel C++ 8.0 for Windows IA-32, Intel C++ 8.0 for Itanium, (http://www.intel.com/), IBM XLC for AIX (Power, PowerPC), Metrowerks Code Warrior 9.1 for Windows (http://www.metrowerks.com/), 和 Comeau C++ 4.3 (http://www.comeaucomputing.com/). Comeau的數據是在它支持的32位平臺(x86, Alpha, SPARC等)上得出的。16位的編譯器的數據在四種DOS配置(tiny, compact, medium, 和 large)下測試得出,用來顯示各種不同代碼和數據指針的大小。MSVC在/vmg的選項下進行了測試,用來顯示“成員指針的全部特性”。(如果你擁有在列表中沒有出現的編譯器,請告知我。非x86處理機下的編譯器測試結果有獨特的價值。)
看著表中的數據,你是不是覺得很驚奇?你可以清楚地看到編寫一段在一些環境中可以運行而在另一些編譯器中不能運行的代碼是很容易的。不同的編譯器之間,它們的內部實現顯然是有很大差別的;事實上,我認為編譯器在實現語言的其他特性上并沒有這樣明顯的差別。對實現的細節進行研究你會發現一些奇怪的問題。
一般,編譯器采取最差的,而且一直使用最普通的形式。比如對于下面這個結構:
// Borland (缺省設置) 和Watcom C++.
struct {
FunctionPointer m_func_address;
int m_delta;
int m_vtable_index; //如果不是虛擬繼承,這個值為0。
};
// Metrowerks CodeWarrior使用了稍微有些不同的方式。
//即使在不允許多重繼承的Embedded C++的模式下,它也使用這樣的結構!
struct {
int m_delta;
int m_vtable_index; // 如果不是虛擬繼承,這個值為-1。
FunctionPointer m_func_address;
};
// 一個早期的SunCC版本顯然使用了另一種規則:
struct {
int m_vtable_index; //如果是一個非虛擬函數(non-virtual function),這個值為0。
FunctionPointer m_func_address; //如果是一個虛擬函數(virtual function),這個值為0。
int m_delta;
};
//下面是微軟的編譯器在未知繼承類型的情況下或者使用/vmg選項時使用的方法:
struct {
FunctionPointer m_func_address;
int m_delta;
int m_vtordisp;
int m_vtable_index; // 如果不是虛擬繼承,這個值為0
};
// AIX (PowerPC)上IBM的XLC編譯器:
struct {
FunctionPointer m_func_address; // 對PowerPC來說是64位
int m_vtable_index;
int m_delta;
int m_vtordisp;
};
// GNU g++使用了一個機靈的方法來進行空間優化
struct {
union {
FunctionPointer m_func_address; // 其值總是4的倍數
int m_vtable_index_2; // 其值被2除的結果總是奇數
};
int m_delta;
};
對于幾乎所有的編譯器,delta和vindex用來調整傳遞給函數的this指針,比如Borland的計算方法是:
adjustedthis = *(this + vindex -1) + delta // 如果vindex!=0
adjustedthis = this + delta // 如果vindex=0
(其中,“*”是提取該地址中的數值,adjustedthis是調整后的this指針??譯者注)
Borland使用了一個優化方法:如果這個類是單一繼承的,編譯器就會知道delta和vindex的值是0,所以它就可以跳過上面的計算方法。
GNU編譯器使用了一個奇怪的優化方法。可以清楚地看到,對于多重繼承來說,你必須查看vtable(虛擬函數表)以獲得voffset(虛擬函數偏移地址)來計算this指針。當你做這些事情的時候,你可能也把函數指針保存在vtable中。通過這些工作,編譯器將m_func_address和m_vtable_index合二為一(即放在一個union中),編譯器區別這兩個變量的方法是使函數指針(m_func_address)的值除以2后結果為偶數,而虛擬函數表索引(m_vtable_index_2)除以2后結果為奇數。它們的計算方法是:
adjustedthis = this + delta
if (funcadr & 1) //如果是奇數
call (* ( *delta + (vindex+1)/2) + 4)
else //如果是偶數
call funcadr
(其中, funcadr是函數地址除以2得出的結果。??譯者注)
Inter的Itanium編譯器(但不是它們的x86編譯器)對虛擬繼承(virtual inheritance)的情況也使用了unknown_inheritance結構,所以,一個虛擬繼承的指針有20字節大小,而不是想象中的16字節。
// Itanium,unknown 和 virtual inheritance下的情況.
struct {
FunctionPointer m_func_address; //對Itanium來說是64位
int m_delta;
int m_vtable_index;
int m_vtordisp;
};
我不能保證Comeau C++使用的是和GNU相同的技術,也不能保證它們是否使用short代替int使這種虛擬函數指針的結構的大小縮小至8個字節。最近發布的Comeau C++版本為了兼容微軟的編譯器也使用了微軟的編譯器關鍵字(我想它也只是忽略這些關鍵字而不對它們進行實質的相關處理罷了)。
Digital Mars編譯器(即最初的Zortech C++到后來的Symantec C++)使用了一種不同的優化方法。對單一繼承類來說,一個成員函數指針僅僅是這個函數的地址。但涉及到更復雜的繼承時,這個成員函數指針指向一個形式轉換函數(thunk function),這個函數可以實現對this指針的必要調整并可用來調用實際的成員函數。每當涉及到多重繼承的時候,每一個成員函數的指針都會有這樣一個形式轉換函數,這對函數調用來說是非常有效的。但是這意味著,當使用多重繼承的時候,子類的成員函數指針向基類成員函數指針的轉換就會不起作用了。可見,這種編譯器對編譯代碼的要求比其他的編譯器要嚴格得多。
很多嵌入式系統的編譯器不允許多重繼承。這樣,這些編譯器就避免了可能出現的問題:一個成員函數指針就是一個帶有隱藏this指針參數的普通函數指針。
微軟"smallest for class"方法的問題
微軟的編譯器使用了和Borland相似的優化方法。它們都使單一繼承的情況具有最優的效率。但不像Borland,微軟在缺省條件下成員函數指針省略了值為0 的指針入口(entry),我稱這種技術為“smallest for class”方法:對單一繼承類來說,一個成員函數指針僅保存了函數的地址(m_func_address),所以它有4字節長。而對于多重繼承類來說,由于用到了偏移地址(m_delta),所以它有8字節長。對虛擬繼承,會用到12個字節。這種方法確實節省空間,但也有其它的問題。
首先,將一個成員函數指針在子類和基類之間進行轉化會改變指針的大小!因此,信息是會丟失的。其次,當一個成員函數指針在它的類定義之前聲明的時候,編譯器必須算出要分配給這個指針多少空間,但是這樣做是不安全的,因為在定義之前編譯器不可能知道這個類的繼承方式。對Intel C++和早期的微軟編譯器來說,編譯器僅僅對指針的大小進行猜測,一旦在源文件中猜測錯誤,你的程序會在運行時莫名其妙地崩潰。所以,微軟的編譯器中增加了一些保留字:__single_inheritance, __multiple_inheritance,和 __virtual_inheritance,并增設了一些編譯器開關(compiler switch),如/vmg,讓所有的成員函數指針有相同的大小,而對原本個頭小的成員函數指針的空余部分用0填充。Borland編譯器也增加了一些編譯器開關,但沒有增加新的關鍵字。Intel的編譯器可以識別Microsoft增加的那些關鍵字,但它在能夠找到類的定義的情況下會對這些關鍵字不做處理。
對于MSVC來說,編譯器需要知道類的vtable在哪兒;通常就會有一個this指針的偏移量(vtordisp),這個值對所有這個類中的成員函數來說是不變的,但對每個類來說會是不同的。對于MSVC,經調整過的this指針是在原this指針的基礎上經過下面的計算得出的:
if (vindex=0) //如果不是虛擬繼承(_virtual_inheritance)
adjustedthis = this + delta
else //如果是
adjustedthis = this + delta + vtordisp + *(*(this + vtordisp) + vindex)
在虛擬繼承的情況下,vtordisp的值并不保存在__virtual_inheritance指針中,而是在發現函數調用的代碼時,編譯器才將其相應的匯編代碼“嵌”進去。但是對于未知類型的繼承,編譯器需要盡可能地通過讀代碼確定它的繼承類型,所以,編譯器將虛擬繼承指針(virtual inheritance pointer)分為兩類(__virtual_inheritance和__unknown_inheritance)。
理論上,所有的編譯器設計者應該在MFP(成員函數指針)的實現上有所變革和突破。但在實際上,這是行不通的,因為這使現在編寫的大量代碼都需要改變。微軟曾發表了一篇非常古老的文章(http://msdn.microsoft.com/archive/en-us/dnarvc/html/jangrayhood.asp)來解釋Visual C++運作的實現細節。這篇文章是Jan Gray寫的,他曾在1990年設計了Microsoft C++的對象模型。盡管這篇文章發表于1994年,但這篇文章仍然很重要??這意味著C++的對象模型在長達15年的時間里(1990年到2004年)沒有絲毫改變。
現在,我想你對成員函數指針的事情已經知道得太多了。要點是什么?我已為你建立了一個規則。雖然各種編譯器的在這方面的實現方法有很大的不同,但是也有一些有用的共同點:不管對哪種形式的類,調用一個成員函數指針生成的匯編語言代碼是完全相同的。有一種特例是使用了“smallest for class”技術的非標準的編譯器,即使是這種情況,差別也是很微小的。這個事實可以讓我們繼續探索怎樣去建立高性能的委托(delegate)。
委托(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++沒有“通用成員函數(generic 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)”的成員函數的方法來實現的。 第一種方法使用了一個邪惡的方法(evil method)。你可以存儲函數指針而不是this指針,這樣當調用“調用器”的函數時,它將this指針轉化為一個靜態函數指針,并調用這個靜態函數。問題是這只是一個戲法,它需要在代碼指針和數據指針之間進行轉換。在一個系統中代碼指針的大小比數據指針大時(比如DOS下的編譯器使用medium內存模式時),這個方法就不管用了。它在目前我知道的所有32位和64位處理器上是管用的。但是因為這種方法還是不太好,所以仍需要改進。 另一種是一個比較安全的方法(safe method),它是將函數指針作為委托的一個附加成員。委托指向自己的成員函數。當委托被復制的時候,這些自引用(self-reference)必須被轉換,而且使“=”和“==”運算符的操作變得復雜。這使委托的大小增至4個字節,并增加了代碼的復雜性,但這并不影響委托的調用速度。 我已經實現了上述兩種方法,兩者都有各自的優點:安全的方法保證了運行的可靠性,而邪惡的方法在支持委托的編譯器下也可能會產生與此相同的匯編代碼。此外,安全的方法可避免我以前討論的在MSVC中使用多重繼承和虛擬繼承時所出現的問題。我在代碼中給出的是“安全的方法”的代碼,但是在我給出的代碼中“邪惡的方法”會通過下面的代碼生效: #define (FASTDELEGATE_USESTATICFUNCTIONHACK) 多目標委托(multiple-target delegate)及其擴展 使用委托的人可能會想使委托調用多個目標函數,這就是多目標委托(multiple-target delegate),也稱作多播委托(multi-cast delegate)。實現這種委托不會降低單一目標委托(single-target delegate)的調用效率,這在現實中是可行的。你只需要為一個委托的第二個目標和后來的更多目標在堆上分配空間就可以了,這意味著需要在委托類中添加一個數據指針,用來指向由該委托的目標函數組成的單鏈表的頭部節點。如果委托只有一個目標函數,將這個目標像以前介紹的方法一樣保存在委托中就行了。如果一個委托有多個目標函數,那么這些目標都保存在空間動態分配的鏈表中,如果要調用函數,委托使用一個指針指向一個鏈表中的目標(成員函數指針)。這樣的話,如果委托中只有一個目標,函數調用存儲單元的個數為1;如果有n(n>0)個目標,則函數調用存儲單元的個數為n+1(因為這時函數指針保存在鏈表中,會多出一個鏈表頭,所以要再加一??譯者注),我認為這樣做最合理。 由多播委托引出了一些問題。怎樣處理返回值?(是將所有返回值類型捆綁在一起,還是忽略一部分?)如果把同一個目標在一個委托中添加了兩次那會發生什么?(是調用同一個目標兩次,還是只調用一次,還是作為一個錯誤處理?)如果你想在委托中刪除一個不在其中的目標應該怎么辦?(是不管它,還是拋出一個異常?) 最重要的問題是在使用委托時會出現無限循環的情況,比如,A委托調用一段代碼,而在這段代碼中調用B委托,而在B委托調用的一段代碼中又會調用A委托。很多事件(event)和信號跟蹤(signal-slot)系統會有一定的方案來處理這種問題。 為了結束我的這篇文章,我的多播委托的實現方案就需要大家等待了。這可以借鑒其他實現中的方法??允許非空返回類型,允許類型的隱式轉換,并使用更簡捷的語法結構。如果我有足夠的興趣我會把代碼寫出來。如果能把我實現的委托和目前流行的某一個事件處理系統結合起來那會是最好不過的事情了(有自愿者嗎?)。 本文代碼的使用 原代碼包括了FastDelegate的實現(FastDelegate.h)和一個demo .cpp的文件用來展示使用FastDelegate的語法。對于使用MSVC的讀者,你可以建立一個空的控制臺應用程序(Console Application)的工程,再把這兩個文件添加進去就好了,對于GNU的使用者,在命令行輸入“gcc demo.cpp”就可以了。 FastDelegate可以在任何參數組合下運行,我建議你在盡可能多的編譯器下嘗試,你在聲明委托的時候必須指明參數的個數。在這個程序中最多可以使用8個參數,若想進行擴充也是很容易的。代碼使用了fastdelegate命名空間,在fastdelegate命名空間中有一個名為detail的內部命名空間。 Fastdelegate使用構造函數或bind()可以綁定一個成員函數或一個靜態(全局)函數,在默認情況下,綁定的值為0(空函數)。可以使用“!”操作符判定它是一個空值。 不像用其他方法實現的委托,這個委托支持等式運算符(==, !=)。 下面是FastDelegateDemo.cpp的節選,它展示了大多數允許的操作。CBaseClass是CDerivedClass的虛基類。你可以根據這個代碼寫出更精彩的代碼,下面的代碼只是說明使用FastDelegate的語法: using namespace fastdelegate; int main(void) { printf("-- FastDelegate demo --/nA no-parameter delegate is declared using FastDelegate0/n/n"); FastDelegate0 noparameterdelegate(&SimpleVoidFunction); noparameterdelegate(); //調用委托,這一句調用SimpleVoidFunction() printf("/n-- Examples using two-parameter delegates (int, char *) --/n/n"); typedef FastDelegate2 MyDelegate; MyDelegate funclist[12]; // 委托初始化,其目標為空 CBaseClass a("Base A"); CBaseClass b("Base B"); CDerivedClass d; CDerivedClass c; // 綁定一個成員函數 funclist[0].bind(&a, &CBaseClass::SimpleMemberFunction); //你也可以綁定一個靜態(全局)函數 funclist[1].bind(&SimpleStaticFunction); //綁定靜態成員函數 funclist[2].bind(&CBaseClass::StaticMemberFunction); // 綁定const型的成員函數 funclist[3].bind(&a, &CBaseClass::ConstMemberFunction); // 綁定虛擬成員函數 funclist[4].bind(&b, &CBaseClass::SimpleVirtualFunction); // 你可以使用”=”來賦值 funclist[5] = MyDelegate(&CBaseClass::StaticMemberFunction); funclist[6].bind(&d, &CBaseClass::SimpleVirtualFunction); //最麻煩的情況是綁定一個抽象虛擬函數(abstract virtual function) funclist[7].bind(&c, &CDerivedClass::SimpleDerivedFunction); funclist[8].bind(&c, &COtherClass::TrickyVirtualFunction); funclist[9] = MakeDelegate(&c, &CDerivedClass::SimpleDerivedFunction); // 你也可以使用構造函數來綁定 MyDelegate dg(&b, &CBaseClass::SimpleVirtualFunction); char *msg = "Looking for equal delegate"; for (int i=0; i<12; i++) { printf("%d :", i); // 可以使用”==” if (funclist[i]==dg) { msg = "Found equal delegate"; }; //可以使用”!”來判應一個空委托 if (!funclist[i]) { printf("Delegate is empty/n"); } else { // 調用生成的經過優化的匯編代碼 funclist[i](i, msg); }; } }; 因為我的代碼利用了C++標準中沒有定義的行為,所以我很小心地在很多編譯器中做了測試。具有諷刺意味的是,它比許多所謂標準的代碼更具有可移植性,因為幾乎所有的編譯器都不是完全符合標準的。目前,核心代碼已成功通過了下列編譯器的測試:
Microsoft Visual C++ 6.0, 7.0 (.NET) and 7.1 (.NET 2003) (including /clr 'managed C++'),
GNU G++ 3.2 (MingW binaries),
Borland C++ Builder 5.5.1,
Digital Mars C++ 8.38 (x86, both 32-bit and 16-bit),
Intel C++ for Windows 8.0,
Metroworks CodeWarrior for Windows 9.1 (in both C++ and EC++ modes)
對于Comeau C++ 4.3 (x86, SPARC, Alpha, Macintosh),能夠成功通過編譯,但不能鏈接和運行。對于Intel C++ 8.0 for Itanium能夠成功通過編譯和鏈接,但不能運行。 此外,我已對代碼在MSVC 1.5 和4.0,Open Watcom WCL 1.2上的運行情況進行了測試,由于這些編譯器不支持成員函數模版,所以對這些編譯器,代碼不能編譯成功。對于嵌入式系統不支持模版的限制,需要對代碼進行大范圍的修改。(這一段是在剛剛更新的原文中添加的??譯者注) 而最終的FastDelegate并沒有進行全面地測試,一個原因是,我有一些使用的編譯器的評估版過期了,另一個原因是??我的女兒出生了!如果有足夠的興趣,我會讓代碼在更多編譯器中通過測試。(這一段在剛剛更新的原文中被刪去了,因為作者目前幾乎完成了全部測試。??譯者注) 總結 為了解釋一小段代碼,我就得為這個語言中具有爭議的一部分寫這么一篇長長的指南。為了兩行匯編代碼,就要做如此麻煩的工作。唉~! 我希望我已經澄清了有關成員函數指針和委托的誤解。我們可以看到為了實現成員函數指針,各種編譯器有著千差萬別的方法。我們還可以看到,與流行的觀點不同,委托并不復雜,并不是高層結構,事實上它很簡單。我希望它能夠成為這個語言(標準C++)中的一部分,而且我們有理由相信目前已被一些編譯器支持的委托,在不久的將來會加入到標準C++的新的版本中(去游說標準委員會!)。 據我所知,以前實現的委托都沒有像我在這里為大家展示的FastDelegate一樣有如此高的性能。我希望我的代碼能對你有幫助。如果我有足夠的興趣,我會對代碼進行擴展,從而支持多播委托(multi-cast delegate)以及更多類型的委托。我在CodeProject上學到了很多,并且這是我第一次為之做出的貢獻。 參考文獻 [GoF] "Design Patterns: Elements of Reusable Object-Oriented Software", E. Gamma, R. Helm, R. Johnson, and J. Vlissides. I've looked at dozens of websites while researching this article. Here are a few of the most interesting ones: 我在寫這篇文章時查看了很多站點,下面只是最有趣的一些站點: [Boost] Delegates can be implemented with a combination of boost::function and boost::bind. Boost::signals is one of the most sophisticated event/messaging system available. Most of the boost libraries require a highly standards-conforming compiler. (http://www.boost.org/) [Loki] Loki provides 'functors' which are delegates with bindable parameters. They are very similar to boost::function. It's likely that Loki will eventually merge with boost. (http://sourceforge.net/projects/loki-lib) [Qt] The Qt library includes a Signal/Slot mechanism (i.e., delegates). For this to work, you have to run a special preprocessor on your code before compiling. Performance is very poor, but it works on compilers with very poor template support. (http://doc.trolltech.com/3.0/signalsandslots.html) [Libsigc++] An event system based on Qt's. It avoids the Qt's special preprocessor, but requires that every target be derived from a base object class (using virtual inheritance - yuck!). (http://libsigc.sourceforge.net/) [Hickey]. An old (1994) delegate implementation that avoids memory allocations. Assumes that all pointer-to-member functions are the same size, so it doesn't work on MSVC. There's a helpful discussion of the code here. (http://www.tutok.sk/fastgl/callback.html) [Haendal]. A website dedicated to function pointers?! Not much detail about member function pointers though. (http://www.function-pointer.org/) [Sutter1] Generalized function pointers: a discussion of how boost::function has been accepted into the new C++ standard. (http://www.cuj.com/documents/s=8464/cujcexp0308sutter/) [Sutter2] Generalizing the Observer pattern (essentially, multicast delegates) using std::tr1::function. Discusses the limitations of the failure of boost::function to provide operator ==. (http://www.cuj.com/documents/s=8840/cujexp0309sutter) [Sutter3] Herb Sutter's Guru of the Week article on generic callbacks. (http://www.gotw.ca/gotw/083.htm) 關于作者Don Clugston 我在澳大利亞的high-tech startup工作,是一個物理學家兼軟件工程師。目前從事將太陽航空艙的硅質晶體玻璃(CSG)薄膜向市場推廣的工作。我從事有關太陽的(solar)研究,平時喜歡做一些軟件(用作數學模型、設備控制、離散事件觸發器和圖象處理等),我最近喜歡使用STL和WTL寫代碼。我非常懷念過去的光榮歲月:)而最重要的,我有一個非常可愛的兒子(2002年5月出生)和一個非常年輕的小姐(2004年5月出生)。 “黑暗不會戰勝陽光,陽光終究會照亮黑暗。” 譯者注 由于本文剛發表不久,作者隨時都有可能對文章或代碼進行更新,若要瀏覽作者對本文的最新內容,請訪問: http://www.codeproject.com/cpp/FastDelegate.asp 點擊以下鏈接下載FastDelegate的源代碼: http://www.codeproject.com/cpp/FastDelegate/FastDelegate_src.zip