`
mengdejun
  • 浏览: 409007 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
社区版块
存档分类
最新评论

c++实现委托

阅读更多

转载自:http://blog.huang-wei.com/2010/08/09/c%E4%B8%AD%E5%AE%9E%E7%8E%B0%E5%A7%94%E6%89%98%EF%BC%88delegate%EF%BC%89/

公司的项目里有用到Don Clugston的FastDelegate,当时只知道是类似boost::function的东西,UI上当watcher用的比较多,所以也没去关注。今天想写个事件触发器时,突然想起这茬子,看来有必要认真的研究学习下了。

搜了下网上关于Delegate的东西,有很多网友自个实现的简易版本,CodeProject上有2个开源项目写的不错,一个就是FastDelegate,还有一个模仿C#的DelegateFastDelegate的 实现依赖于编译器,作者NB的研究了各种编译器在各种平台上生成的汇编代码,将C++中神秘的成员函数调用还原普通的函数调用,其调用的汇编代码用他的框 架在运行时“生成”,使任何函数都可以无差别的挂接在他的Delegate之上。如MicrosoftVirtualMFP这样的编译后得到的函数结构。

作者原文是英文的,网上已有翻译,我这就贴下,作为笔记。

成员函数指针与高性能的C++委托(上篇)

文: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*为参数的函数,这个函数返回一个浮点值,声明如下:

1
2
3
4
5
6
7
8
9
10
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 *)的函数,这样做就可以了:

1 2 3
my_func_ptr = some_func; // 当你想调用它所指向的函数时,你可以这样写: (*my_func_ptr)(7, "Arbitrary String");

你可以将一种类型的函数指针转换成另一种函数指针类型,但你不可以将一个函数指针指向一个void *型的数据指针。其他的转换操作就不用详叙了。一个函数指针可以被设置为NULL来表明它是一个空指针。所有的比较运算符(==, !=, <, >, <=, >=)都可以使用,可以使用”==NULL″或通过一个显式的布尔转换来测试指针是否为空(NULL)。

在C语言中,函数指针通常用来像qsort一样将函数作为参数,或者作为Windows系统函数的回调函数等等。函数指针还有很多其他的应 用。函数指针的实现很简单:它们只是”代码指针(code pointer)”,它们体现在汇编语言中是用来保存子程序代码的首地址。而这种函数指针的存在只是为了保证使用了正确的调用规范。

成员函数指针

在C++程序中,很多函数是成员函数,即这些函数是某个类中的一部分。你不可以像一个普通的函数指针那样指向一个成员函数,正确的做法应该是,你必须使用一个成员函数指针。一个成员函数的指针指向类中的一个成员函数,并和以前有相同的参数,声明如下:

1 2 3 4
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 *)的函数,你可以这样写:

1
my_memfunc_ptr = &(SomeClass::some_member_func);

很多编译器(比如MSVC)会让你去掉”&”,而其他一些编译器(比如GNU G++)则需要添加”&”,所以在手写程序的时候我建议把它添上。若要调用成员函数指针,你需要先建立SomeClass的一个实例,并使用特殊 操作符”->*”,这个操作符的优先级较低,你需要将其适当地放入圆括号内。

1
2
3
4
5
6
SomeClass *x = new SomeClass;
<pre>(x-&gt;*my_memfunc_ptr)(6, "Another Arbitrary Parameter");

//如果类在栈上,你也可以使用".*"运算符。
SomeClass y;
(y.*my_memfunc_ptr)(15, "Different parameters this time");

不要怪我使用如此奇怪的语法——看起来C++的设计者对标点符号有着由衷的感情!C++相对于C增加了三种特殊运算符来支持成员指 针。”::*”用于指针的声明,而”->*”和”.*”用来调用指针指向的函数。这样看起来对一个语言模糊而又很少使用的部分的过分关注是多余 的。(你当然可以重载”->*”这些运算符,但这不是本文所要涉及的范围。)

一个成员函数指针可以被设置成0,并可以使用”==”和”!=”比较运算符,但只能限定在同一个类中的成员函数的指针之间进行这样的比较。 任何成员函数指针都可以和0做比较以判断它是否为空。与函数指针不同,不等运算符(<, >, <=, >=)对成员函数指针是不可用的。

成员函数指针的怪异之处

成员函数指针有时表现得很奇怪。首先,你不可以用一个成员函数指针指向一个静态成员函数,你必须使用普通的函数指针才行(在这里”成员函数 指针”会产生误解,它实际上应该是”非静态成员函数指针”才对)。其次,当使用类的继承时,会出现一些比较奇怪的情况。比如,下面的代码在MSVC下会编 译成功(注意代码注释):

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
#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++的标准化的过程中,在涉及继承的类的成员函数指针时,对于将成员函数指 针转化为基类的成员函数指针还是转化为子类成员函数指针的问题和是否可以将一个类的成员函数指针转化为另一个不相关的类的成员函数指针的问题,人们曾有过 很激烈的争论。然而不幸的是,在标准委员会做出决定之前,不同的编译器生产商已经根据自己对这些问题的不同的回答实现了自己的编译器。根据标准(第 5.2.10/9节),你可以使用reinterpret_cast在一个成员函数指针中保存一个与本来的类不相关的类的成员函数。有关成员函数指针转换 的问题的最终结果也没有确定下来。你现在所能做的还是像以前那样——将成员函数指针转化为本类的成员函数的指针。在文章的后面我会继续讨论这个问题,因为 这正是各个编译器对这样一个标准没有达成共识的一个话题。

在一些编译器中,在基类和子类的成员函数指针之间的转换时常有怪事发生。当涉及到多重继承时,使用reinterpret_cast将子类 转换成基类时,对某一特定编译器来说有可能通过编译,而也有可能通不过编译,这取决于在子类的基类列表中的基类的顺序!下面就是一个例子:

1 2 3 4 5 6 7
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)是可行的,但这不是标准!)。第三,允许转换而不允许调用是完全 没有用处的,只有转换和调用都可行,才能方便而有效地实现委托,从而使这种语言受益。
为了让你确信这一具有争议的论断,考虑一下在一个文件中只有下面的一段代码,这段代码是合法的:

1 2 3 4
class SomeClass; typedef void (SomeClass::* SomeClassFunction)(void); void Invoke(SomeClass *pClass, SomeClassFunction funcptr) { (pClass-&gt;*funcptr)(); };

注意到编译器必须生成汇编代码来调用成员函数指针,其实编译器对SomeClass类一无所知。显然,除非链接器进行了一些极端精细的优化 措施,否则代码会忽视类的实际定义而能够正确地运行。而这造成的直接后果是,你可以”安全地”调用从完全不同的其他类中转换过来的成员函数指针。

为解释我的断言的另一半——转换并不能按照标准所说的方式进行,我需要在细节上讨论编译器是怎样实现成员函数指针的。我同时会解释为什么使 用成员函数指针的规则具有如此严格的限制。获得详细论述成员函数指针的文档不是太容易,并且大家对错误的言论已经习以为常了,所以,我仔细检查了一系列编译器生成的汇编代码……

成员函数指针与高性能的C++委托(中篇)

Member Function Pointers and the Fastest Possible C++ Delegates

撰文: Don Clugston

翻译:周翔

成员函数指针——为什么那么复杂?

类的成员函数和标准的 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个类定义如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
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处理机下的编译器测试结果有独特的价值。)

看着表中的数据,你是不是觉得很惊奇?你可以清楚地看到编写一段在一些环境中可以运行而在另一些编译器中不能运行的代码是很容易的。不同的编译器之间,它们 的内部实现显然是有很大差别的;事实上,我认为编译器在实现语言的其他特性上并没有这样明显的差别。对实现的细节进行研究你会发现一些奇怪的问题。

一般,编译器采取最差的,而且一直使用最普通的形式。比如对于下面这个结构:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
// 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的计算方法是:

1 2
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后结果为奇数。它们的计 算方法是:

1 2 3 4 5
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字节。

1 2 3 4 5 6 7
// 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指针的基础上经过下面的计算得出的:

1 2 3 4
if (vindex=0) //如果不是虚拟继承( _virtual_inheritance)   adjustedthis = this + delta else //如果是   adjustedthis = this + delta + vtordisp + *(*(this + vtordisp) + vindex)

在虚拟继承的情况下, vtordisp的值并不保存在 __virtual_inheritance指针中,而是在发现函数调用的代码时,编译器 才将其相应的汇编代码“嵌”进去。但是

分享到:
评论

相关推荐

    c++实现委托和事件

    以下是对这两个关键工具的详细解释,以及如何用它们来实现委托和事件的机制。 ### `std::function` `std::function`是C++11引入的一个通用函数对象容器。它可以存储任何可调用对象,包括函数指针、成员函数指针、...

    非托管C++委托实现

    以下是基于非托管C++实现委托可能涉及的关键知识点: 1. **模板(Template)**:C++的模板是一种泛型编程工具,可以用于创建泛型类或函数。在实现委托时,模板可以用来确保委托实例可以与任何具有匹配签名的方法...

    C++中实现委托(delegate)

    网上有很多关于C++ delegate机制的文章,但都讲的是函数指针的内容,上周就C++中实现C#的delegate机制的问题研究了好几天,查看了很多相关资料,才解决了问题,现将我写的C++ delegate测试程序贴出来,希望能帮到有...

    C++实现委托及分析

    回过头来说C#的委托,在C语言里面是函数指针,给个概念是回调,只不过官方说C#的实现是更加安全的函数指针,我是目前理解不能了,因为我还没有达到那个高度。  来关注我们的主角:C++。针对对象

    C++中委托的实现方法

    C++模拟实现的类似于C#的委托。 相对于前一版本,此版本使用STL封装了参数列表的变化,支持包含0到9个参数的函数的委托形式,此外增加了对于全局函数(类静态函数)的委托的支持。 在Visual C++ 6.0下测试通过。

    C++事件委托

    C++事件委托机制是指在C++编程语言中实现事件处理的机制,通过委托机制,可以使得事件处理更加灵活和可靠。本文将详细介绍事件委托机制的需求、演变过程、模板化的事件委托机制,并对其原理进行深入分析。 一、事件...

    C++下实现委托的代码

    下面我们将详细探讨如何在C++中实现委托的代码。 首先,理解委托的基本思想:一个委托对象可以持有对另一个函数或成员函数的引用,并能够在适当的时候调用它。为了在C++中实现这个概念,我们可以使用函数指针、仿...

    c++观察者模式与委托机制的结合

    描述中提到使用宏来实现委托,这是一种简化代码的方式,可以方便地创建无参数和多参数的委托。 在Qt框架中,观察者模式和委托机制有很好的支持。Qt的信号和槽机制就是一种委托实现,它允许对象之间通过信号进行通信...

    线程委托库C++源码(内置多线程浏览器实现)

    一个线程委托库的实现源码,最大的优点是实现多线程不再困难,线程同步和管理不再头疼,同时内置了...源码使用了C++模板库,内部采用ATL实现。使用时只需关注压缩文件中的Raptor.h文件,这是封装好的类,易于使用。

    C++的委托实现(MyGUI版本)

    MyGUI是一个流行的2D图形用户界面库,它提供了一种基于C++的委托实现,使得用户可以方便地在事件处理中使用自定义的回调函数。本篇文章将详细介绍MyGUI中的委托实现,并通过`sample.cpp`提供的使用范例,解析如何在...

    设计模式之委托模式的demo

    使用c++实现的委托模式源代码,供学习设计模式参考使用

    实现高效C++委托的代码

    `FastDelegate.h`文件是实现委托的核心头文件。在这个文件中,Don Clugston使用模板元编程技术来实现高效的函数调用。元编程是在编译时执行的程序,它能生成其他类型的代码。`FastDelegate`类模板可以接受任何数量和...

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

    在C++中实现类似C#或Java等面向对象语言中的委托功能是一项挑战但也极具价值的技术实践。本文将深入探讨如何利用成员函数指针来创建高效的C++委托,并分析其背后的原理和技术细节。 #### 成员函数指针的概念 在C++...

    C++ 使用STL和函数指针 实现的委托

    C++模拟实现的类似于C#的委托。 使用STL封装类名称的变化,使用函数指针封装函数名称的变化。 在Visual C++ 6.0下测试通过。 使用的时候只需要关注IFunctor和IDelegate两个接口即可。

    C++实现C#事件机制

    在C++编程中,模拟C#的事件机制是一项挑战,因为C++本身并不直接支持像C#那样的事件和委托概念。然而,通过巧妙地利用函数指针和一些设计模式,可以实现类似的功能。本文将深入探讨如何在C++中实现C#事件机制的关键...

    C++ - 使用标准库实现事件和委托,信号和槽机制

    在C++中,虽然没有内置的事件和委托支持,但可以通过一些设计模式或第三方库来实现,例如使用STL(Standard Template Library)中的函数对象和回调函数。本文将探讨如何利用C++标准库来模拟事件和委托的概念,尤其是...

    C++11 function 委托机制 观察者模式,适用于界面开发,lambda表达式

    首先,C++11的函数委托是通过函数对象(functor)或者函数指针实现的。它提供了一种方式来保存一个函数或方法的引用,以便稍后调用。在描述的场景中,这个功能可能被用于在界面的初始化(OnInitDialog)时添加委托,...

    在C++中模拟委托事件方法的例子免资源分

    代码是文章《在C++中模拟委托事件方法》的例子,文章地址为http://blog.csdn.net/gogogo/article/details/6996290,详述了通过静态函数、接口、类模板、类模板事件链等不同方式实现其他语言中的委托事件模式,对于...

    C#委托与C++回调函数处理

    C#引入了委托(Delegate)的概念,而C++则依赖于回调函数(Callback Function)来实现类似的功能。这两者都是为了实现异步操作或者在不同组件之间传递控制权。本文将深入探讨C#中的委托以及C++中的回调函数,并阐述...

Global site tag (gtag.js) - Google Analytics