网站设计师证书,新注册公司网站建设,购物小程序源码,企业网站首页效果图设计与制作成员函数指针与高性能的C委托 http://www.cnblogs.com/jans2002/archive/2006/10/13/528160.html Member Function Pointers and the Fastest Possible C Delegates 撰文#xff1a;Don Clugston 翻译#xff1a;周翔 引子 标准C中没有真正的面向对象的函数指针。这一点对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可视化组建库VCLVisual 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_castBase1mfp(x)是合法的而static_castBase2mfp(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 // 如果vindex0 其中“*”是提取该地址中的数值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 (vindex1)/2) 4) else //如果是偶数 call funcadr 其中 funcadr是函数地址除以2得出的结果。——译者注 Inter的Itanium编译器但不是它们的x86编译器对虚拟继承virtual inheritance的情况也使用了unknown_inheritance结构所以一个虚拟继承的指针有20字节大小而不是想象中的16字节。 // Itaniumunknown 和 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 (vindex0) //如果不是虚拟继承_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的迫切需求