好文轉載:成員函式指標與高效能的C++委託
和成員函式指標不同,你不難發現委託的用處。最重要的,使用委託可以很容易地實現一個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
(全文完)