成員函數指針與高性能的C++委託(下篇)

 

成員函數指針與高性能的C++委託(下篇)

Member Function Pointers and the Fastest Possible C++ Delegates

撰文:

Don Clugston

翻譯:周翔

(接中篇)

委託(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__unhookevent_sourceevent_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_inheritance16字節)類型的時候,也會採用類似的計算方法。

對於微軟和英特爾的編譯器中採用不標準

12字節的虛擬繼承類型的指針的情況,我引發了一個編譯時錯誤(compile-time error),因爲需要一個特定的運行環境(workaround)。如果你在MSVC中使用虛擬繼承,要在聲明類之前使用FASTDELEGATEDECLARE宏。而這個類必須使用unknown_inheritance(未知繼承類型)指針(這相當於一個假定的__unknown_inheritance關鍵字)。例如:

FASTDELEGATEDECLARE(CDerivedClass)

class CDerivedClass : virtual public CBaseClass1, virtual public CBaseClass2 {

// : (etc)

};

這個宏和一些常數的聲明是在一個隱藏的命名空間中實現的,這樣在其他編譯器中使用時也是安全的。

MSVC7.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;如果有nn>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的節選,它展示了大多數允許的操作。CBaseClassCDerivedClass的虛基類。你可以根據這個代碼寫出更精彩的代碼,下面的代碼只是說明使用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.0Open 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)研究,平時喜歡做一些軟件(用作數學模型、設備控制、離散事件觸發器和圖象處理等),我最近喜歡使用STLWTL寫代碼。我非常懷念過去的光榮歲月:)而最重要的,我有一個非常可愛的兒子(20025月出生)和一個非常年輕的小姐(20045月出生)。

“黑暗不會戰勝陽光,陽光終究會照亮黑暗。”

譯者注

由於本文剛發表不久,作者隨時都有可能對文章或代碼進行更新,若要瀏覽作者對本文的最新內容,請訪問:

http://www.codeproject.com/cpp/FastDelegate.asp

點擊以下鏈接下載

FastDelegate的源代碼:

http://www.codeproject.com/cpp/FastDelegate/FastDelegate_src.zip
發佈了25 篇原創文章 · 獲贊 0 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章