日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

成员函数指针与高性能的C++委托

發布時間:2023/12/4 c/c++ 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 成员函数指针与高性能的C++委托 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

成員函數指針與高性能的C++委托

http://www.cnblogs.com/jans2002/archive/2006/10/13/528160.html

Member Function Pointers and the Fastest Possible C++ Delegates

?

撰文:Don Clugston

翻譯:周翔

?

引子

標準C++中沒有真正的面向對象的函數指針。這一點對C++來說是不幸的,因為面向對象的指針(也叫做“閉包(closure)”或“委托(delegate)”)在一些語言中已經證明了它寶貴的價值。在Delphi (Object Pascal)中,面向對象的函數指針是Borland可視化組建庫(VCL,Visual Component Library)的基礎。而在目前,C#使“委托”的概念日趨流行,這也正顯示出C#這種語言的成功。在很多應用程序中,“委托”簡化了松耦合對象的設計模式[GoF]。這種特性無疑在標準C++中也會產生很大的作用。

很遺憾,C++中沒有“委托”,它只提供了成員函數指針(member function pointers。很多程序員從沒有用過函數指針,這是有特定的原因的。因為函數指針自身有很多奇怪的語法規則(比如“->*”和“.*”操作符),而且很難找到它們的準確含義,并且你會找到更好的辦法以避免使用函數指針。更具有諷刺意味的是:事實上,編譯器的編寫者如果實現“委托”的話會比他費勁地實現成員函數指針要容易地多!

在這篇文章中,我要揭開成員函數指針那“神秘的蓋子”。在扼要地重述成員函數指針的語法和特性之后,我會向讀者解釋成員函數指針在一些常用的編譯器中是怎樣實現的,然后我會向大家展示編譯器怎樣有效地實現“委托”。最后我會利用這些精深的知識向你展示在C++編譯器上實現優化而可靠的“委托”的技術。比如,在Visual C++(6.0, .NET, and .NET 2003)中對單一目標委托(single-target delegate)的調用,編譯器僅僅生成兩行匯編代碼!

?

函數指針

下面我們復習一下函數指針。在C和C++語言中,一個命名為my_func_ptr的函數指針指向一個以一個int和一個char*為參數的函數,這個函數返回一個浮點值,聲明如下:

float (*my_func_ptr)(int, char *);

為了便于理解,我強烈推薦你使用typedef關鍵字。如果不這樣的話,當函數指針作為一個函數的參數傳遞的時候,程序會變得晦澀難懂。這樣的話,聲明應如下所示:

typedef float (*MyFuncPtrType)(int, char *);

MyFuncPtrType my_func_ptr;

應注意,對每一個函數的參數組合,函數指針的類型應該是不同的。在Microsoft Visual C++(以下稱MSVC)中,對三種不同的調用方式有不同的類型:__cdecl, __stdcall, 和__fastcall。如果你的函數指針指向一個型如float some_func(int, char *)的函數,這樣做就可以了:

my_func_ptr = some_func;

當你想調用它所指向的函數時,你可以這樣寫:

(*my_func_ptr)(7, "Arbitrary String");

你可以將一種類型的函數指針轉換成另一種函數指針類型,但你不可以將一個函數指針指向一個void *型的數據指針。其他的轉換操作就不用詳敘了。一個函數指針可以被設置為0來表明它是一個空指針。所有的比較運算符(==, !=, <, >, <=, >=)都可以使用,可以使用“==

在C語言中,函數指針通常用來像qsort一樣將函數作為參數,或者作為Windows系統函數的回調函數等等。函數指針還有很多其他的應用。函數指針的實現很簡單:它們只是“代碼指針(code pointer)”,它們體現在匯編語言中是用來保存子程序代碼的首地址。而這種函數指針的存在只是為了保證使用了正確的調用規范。

?

成員函數指針

在C++程序中,很多函數是成員函數,即這些函數是某個類中的一部分。你不可以像一個普通的函數指針那樣指向一個成員函數,正確的做法應該是,你必須使用一個成員函數指針。一個成員函數的指針指向類中的一個成員函數,并和以前有相同的參數,聲明如下:

float (SomeClass::*my_memfunc_ptr)(int, char *);

對于使用const關鍵字修飾的成員函數,聲明如下:

float (SomeClass::*my_const_memfunc_ptr)(int, char *) const;

注意使用了特殊的運算符(::*),而“SomeClass”是聲明中的一部分。成員函數指針有一個可怕的限制:它們只能指向一個特定的類中的成員函數。對每一種參數的組合,需要有不同的成員函數指針類型,而且對每種使用const修飾的函數和不同類中的函數,也要有不同的函數指針類型。在MSVC 中,對下面這四種調用方式都有一種不同的調用類型:

__cdecl, __stdcall, __fastcall, 和 __thiscall。

(__thiscall是缺省的方式,有趣的是,在任何官方文檔中從沒有對__thiscall關鍵字的詳細描述,但是它經常在錯誤信息中出現。如果你顯式地使用它,你會看到“它被保留作為以后使用(it is reserved for future use)”的錯誤提示。)

如果你使用了成員函數指針,你最好使用typedef以防止混淆。將函數指針指向型如float SomeClass::some_member_func(int, char *)的函數,你可以這樣寫:

my_memfunc_ptr = &SomeClass::some_member_func;

很多編譯器(比如MSVC)會讓你去掉“&”,而其他一些編譯器(比如GNU G++)則需要添加“&”,所以在手寫程序的時候我建議把它添上。若要調用成員函數指針,你需要先建立SomeClass的一個實例,并使用特殊操作符“->*”,這個操作符的優先級較低,你需要將其適當地放入圓括號內。

SomeClass *x = new SomeClass;

(x->*my_memfunc_ptr)(6, "Another Arbitrary Parameter");

//如果類在棧上,你也可以使用“.*”運算符。

SomeClass y;

(y.*my_memfunc_ptr)(15, "Different parameters this time");

不要怪我使用如此奇怪的語法——看起來C++的設計者對標點符號有著由衷的感情!C++相對于C增加了三種特殊運算符來支持成員指針。“::*”用于指針的聲明,而“->*”和“.*”用來調用指針指向的函數。這樣看起來對一個語言模糊而又很少使用的部分的過分關注是多余的。(你當然可以重載 “->*”這些運算符,但這不是本文所要涉及的范圍。)

一個成員函數指針可以被設置成0,并可以使用“==”和“!=”比較運算符,但只能限定在同一個類中的成員函數的指針之間進行這樣的比較。任何成員函數指針都可以和0做比較以判斷它是否為空。與函數指針不同,不等運算符(<, >, <=, >=)對成員函數指針是不可用的。

?

成員函數指針的怪異之處

成員函數指針有時表現得很奇怪。

首先,你不可以用一個成員函數指針指向一個靜態成員函數,你必須使用普通的函數指針才行(在這里“成員函數指針”會產生誤解,它實際上應該是“非靜態成員函數指針”才對)。

其次,當使用類的繼承時,會出現一些比較奇怪的情況。比如,下面的代碼在MSVC下會編譯成功(注意代碼注釋):

#include “stdio.h”

class SomeClass {

public:

virtual void some_member_func(int x, char *p) {

printf("In SomeClass"); };

};

class DerivedClass : public SomeClass {

public:

// 如果你把下一行的注釋銷掉,帶有 line (*)的那一行會出現錯誤

// virtual void some_member_func(int x, char *p) { printf("In DerivedClass"); };

};

int main() {

//聲明SomeClass的成員函數指針

typedef void (SomeClass::*SomeClassMFP)(int, char *);

SomeClassMFP my_memfunc_ptr;

my_memfunc_ptr = &DerivedClass::some_member_func; // ---- line (*)

return 0;

}

奇怪的是,&DerivedClass::some_member_func是一個SomeClass類的成員函數指針,而不是 DerivedClass類的成員函數指針!(一些編譯器稍微有些不同:比如,對于Digital Mars C++,在上面的例子中,&DerivedClass::some_member_func會被認為沒有定義。)但是,如果在 DerivedClass類中重寫(override)了some_member_func函數,代碼就無法通過編譯,因為現在的&DerivedClass::some_member_func已成為DerivedClass類中的成員函數指針!

成員函數指針之間的類型轉換是一個討論起來非常模糊的話題。在C++的標準化的過程中,在涉及繼承的類的成員函數指針時,對于將成員函數指針轉化為基類的成員函數指針還是轉化為子類成員函數指針的問題是否可以將一個類的成員函數指針轉化為另一個不相關的類的成員函數指針的問題,人們曾有過很激烈的爭論。然而不幸的是,在標準委員會做出決定之前,不同的編譯器生產商已經根據自己對這些問題的不同的回答實現了自己的編譯器。根據標準(第

在一些編譯器中,在基類和子類的成員函數指針之間的轉換時常有怪事發生。當涉及到多重繼承時,使用reinterpret_cast將子類轉換成基類時,對某一特定編譯器來說有可能通過編譯,而也有可能通不過編譯,這取決于在子類的基類列表中的基類的順序!下面就是一個例子:

class Derived: public Base1, public Base2 // 情況 (a)

class Derived2: public Base2, public Base1 // 情況 (b)

typedef void (Derived::* Derived_mfp)();

typedef void (Derived2::* Derived2_mfp)();

typedef void (Base1::* Base1mfp) ();

typedef void (Base2::* Base2mfp) ();

Derived_mfp x;

對于情況(a)static_cast<Base1mfp>(x)是合法的,而static_cast<Base2mfp>(x)則是錯誤的。然而情況(b)卻與之相反。你只可以安全地將子類的成員函數指針轉化為第一個基類的成員函數指針!如果你要實驗一下,MSVC會發出C4407號警告,而Digital Mars C++會出現編譯錯誤。如果用reinterpret_cast代替static_cast這兩個編譯器都會發生錯誤,但是兩種編譯器對此有著不同的原因。但是一些編譯器對此細節置之不理,大家可要小心了!

標準C++中另一條有趣的規則是:你可以在類定義之前聲明它的成員函數指針。這對一些編譯器會有一些無法預料的副作用。我待會討論這個問題,現在你只要知道要盡可能得避免這種情況就是了。

值得注意的是,就像成員函數指針,標準C++中同樣提供了成員數據指針(member data pointer)。它們具有相同的操作符,而且有一些實現原則也是相同的。它們用在stl::stable_sort的一些實現方案中,而對此很多其他的應用我就不再提及了。

?

成員函數指針的使用

現在你可能會覺得成員函數指針是有些奇異。但它可以用來做什么呢?對此我在網上做了非常廣泛的調查。最后我總結出使用成員函數指針的兩點原因:

  • 用來做例子給C++初學者看,幫助它們學習語法;或者
  • 為了實現“委托(delegate)”!

成員函數指針在STL和Boost庫的單行函數適配器(one-line function adaptor)中的使用是微不足道的,而且允許你將成員函數和標準算法混合使用。但是它們最重要的應用是在不同類型的應用程序框架中,比如它們形成了MFC消息系統的核心。

當你使用MFC的消息映射宏(比如ON_COMMAND)時,你會組裝一個包含消息ID和成員函數指針(型如:CCmdTarget::*成員函數指針)的序列。這是MFC類必須繼承CCmdTarget才可以處理消息的原因之一。但是,各種不同的消息處理函數具有不同的參數列表(比如OnDraw 處理函數的第一個參數的類型為CDC *),所以序列中必須包含各種不同類型的成員函數指針。

MFC是怎樣做到這一點的呢?MFC利用了一個可怕的編譯器漏洞(hack),它將所有可能出現的成員函數指針放到一個龐大的聯合(union)中,從而避免了通常需要進行的C++類型匹配檢查。(看一下afximpl.h和cmdtarg.cpp中名為MessageMapFunctions的union,你就會發現這一恐怖的事實。)

因為MFC有如此重要的一部分代碼,所以事實是,所有的編譯器都為這個漏洞開了綠燈。(但是,在后面我們會看到,如果一些類用到了多重繼承,這個漏洞在MSVC中就不會起作用,這正是在使用MFC時只能必須使用單一繼承的原因。

在boost::function中有類似的漏洞(但不是太嚴重)。看起來如果你想做任何有關成員函數指針的比較有趣的事,你就必須做好與這個語言的漏洞進行挑戰的準備。要是你想否定C++的成員函數指針設計有缺陷的觀點,看來是很難的。

在寫這篇文章中,我有一點需要指明:“允許成員函數指針之間進行轉換(cast),而不允許在轉換完成后調用其中的函數”,把這個規則納入C++的標準中是可笑的。

首先,很多流行的編譯器對這種轉換不支持(所以,轉換是標準要求的,但不是可移植的)。

其次,所有的編譯器,如果轉換成功,調用轉換后的成員函數指針時仍然可以實現你預期的功能:那編譯器就沒有所謂的“undefined behavior(未定義的行為)”這類錯誤出現的必要了(調用(Invocation)是可行的,但這不是標準!)。

第三,允許轉換而不允許調用是完全沒有用處的,只有轉換和調用都可行,才能方便而有效地實現委托,從而使這種語言受益。

為了讓你確信這一具有爭議的論斷,考慮一下在一個文件中只有下面的一段代碼,這段代碼是合法的:

class SomeClass;

typedef void (SomeClass::* SomeClassFunction)(void);

void Invoke(SomeClass *pClass, SomeClassFunction funcptr) {(pClass->*funcptr)(); };

注意到編譯器必須生成匯編代碼來調用成員函數指針,其實編譯器對SomeClass類一無所知。顯然,除非鏈接器進行了一些極端精細的優化措施,否則代碼會忽視類的實際定義而能夠正確地運行。而這造成的直接后果是,你可以“安全地”調用從完全不同的其他類中轉換過來的成員函數指針。

為解釋我的斷言的另一半——轉換并不能按照標準所說的方式進行,我需要在細節上討論編譯器是怎樣實現成員函數指針的。我同時會解釋為什么使用成員函數指針的規則具有如此嚴格的限制。獲得詳細論述成員函數指針的文檔不是太容易,并且大家對錯誤的言論已經習以為常了,所以,我仔細檢查了一系列編譯器生成的匯編代碼……

?

成員函數指針——為什么那么復雜?

類的成員函數和標準的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++委托的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。