65.9K
CodeProject 正在变化。 阅读更多。
Home

成员函数指针和最快的 C++ 委托

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (714投票s)

2004年5月24日

CPOL

49分钟阅读

viewsIcon

2931354

downloadIcon

25096

一篇关于成员函数指针的综合教程,以及一个仅生成两个ASM指令的委托实现!

俄语翻译

我很高兴地宣布,Denis Bulichenko已将本文翻译成俄语!它已在RSDN上发布。完整的文章很快将在网上提供。我非常感谢你,Denis。

引言

标准C++没有真正的面向对象函数指针。这是不幸的,因为面向对象函数指针,也称为“闭包”或“委托”,已经在类似的语言中证明了它们的价值。在Delphi(Object Pascal)中,它们是Borland的Visual Component Library(VCL)的基础。最近,C#普及了委托的概念,为其语言的成功做出了贡献。对于许多应用程序而言,委托简化了由非常松散耦合的对象组成的优雅设计模式(观察者、策略、状态[GoF])的使用。毫无疑问,这样的功能在标准C++中将非常有用。

C++没有委托,只有成员函数指针。大多数C++程序员从未用过成员函数指针,这并非没有道理。它们有自己奇怪的语法(例如`->*`和`.*`运算符),很难找到关于它们的准确信息,而且它们能做的很多事情都可以用其他方式做得更好。这有点令人尴尬:对于编译器编写者来说,实现真正的委托比实现成员函数指针更容易!

在本文中,我将“揭开”成员函数指针的面纱。在回顾了成员函数指针的语法和特点后,我将解释常用编译器如何实现成员函数指针。我将展示编译器如何有效地实现委托。最后,我将展示我是如何利用这些秘密知识来实现一个在大多数C++编译器上效率最高的委托实现的。例如,在Visual C++(6.0、.NET和.NET 2003)上调用一个单目标委托,仅生成两行汇编代码!

函数指针

我们从回顾函数指针开始。在C中,以及因此在C++中,一个名为`my_func_ptr`的函数指针,它指向一个接受`int`和`char*`并返回`float`的函数,声明如下:

float (*my_func_ptr)(int, char *);
// To make it more understandable, I strongly recommend that you use a typedef.
// Things can get particularly confusing when
// the function pointer is a parameter to a function.
// The declaration would then look like this:
typedef float (*MyFuncPtrType)(int, char *);
MyFuncPtrType my_func_ptr;

请注意,每种参数组合都有不同类型的函数指针。在MSVC上,三种不同的调用约定(`__cdecl`、`__stdcall`和`__fastcall`)也各有不同的函数指针类型。您可以通过以下方式使您的函数指针指向函数`float some_func(int, char*)`:

 my_func_ptr = some_func;

当您想调用存储的函数时,您这样做:

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

您可以将一种类型的函数指针强制转换为另一种类型。但是,您不能将函数指针转换为`void*`数据指针。其他允许的操作都是微不足道的。函数指针可以设置为0,以标记它为null指针。所有比较运算符(`==`、`!=`、`<`、`>`、`<=`、`>=`)都可以使用,您还可以通过`==0`或隐式转换为`bool`来测试null指针。有趣的是,函数指针可以用作非类型模板参数。这与类型参数根本不同,也与整数非类型参数不同。它是基于*名称*而不是类型或值实例化的。并非所有编译器都支持基于名称的模板参数,甚至并非所有支持部分模板特化的编译器都支持。

在C中,函数指针最常见的用途是作为库函数(如`qsort`)的参数,以及作为Windows函数的*回调*等。它们还有许多其他应用。函数指针的实现很简单:它们只是“代码指针”:它们保存汇编语言例程的起始地址。不同类型的函数指针仅用于确保使用正确的调用约定。

成员函数指针

在C++程序中,大多数函数都是成员函数;也就是说,它们属于一个类。您不能使用普通函数指针指向成员函数;相反,您必须使用成员函数指针。指向类`SomeClass`的成员函数的成员函数指针,具有与前面相同的参数,声明如下:

float (SomeClass::*my_memfunc_ptr)(int, char *);
// For const member functions, it's declared like this:
float (SomeClass::*my_const_memfunc_ptr)(int, char *) const;

请注意,使用了特殊运算符(`::*`),并且`SomeClass`是声明的一部分。成员函数指针有一个糟糕的限制:它们只能指向单个类的成员函数。每种参数组合、两种`const`类型和每个类都有不同类型的成员函数指针。在MSVC上,四种不同的调用约定(`__cdecl`、`__stdcall`、`__fastcall`和`__thiscall`)也各有不同的类型。(`__thiscall`是默认的。有趣的是,没有文档记录的`__thiscall`关键字,但它有时会出现在错误消息中。如果显式使用它,您将收到一条错误消息,指出它保留供将来使用。)如果您使用成员函数指针,则应始终使用`typedef`以避免混淆。

您可以通过以下方式使函数指针指向函数`float SomeClass::some_member_func(int, char*)`:

 my_memfunc_ptr = &SomeClass::some_member_func;
 // This is the syntax for operators:
 my_memfunc_ptr = &SomeClass::operator !;
 // There is no way to take the address of a constructor or destructor

某些编译器(最显著的是MSVC 6和7)允许您省略`&`,尽管这是非标准的且令人困惑的。更符合标准编译器的编译器(例如,GNU G++和MSVC 8(又名VS 2005))需要它,所以您绝对应该加上它。要调用成员函数指针,您需要提供`SomeClass`的一个实例,并且必须使用特殊运算符`->*`。此运算符的优先级较低,因此您需要将其括在括号中:

  SomeClass *x = new SomeClass;
  (x->*my_memfunc_ptr)(6, "Another Arbitrary Parameter");
// You can also use the .* operator if your class is on the stack.
  SomeClass y;
  (y.*my_memfunc_ptr)(15, "Different parameters this time");

不要因为语法而责怪我——似乎C++的设计者之一非常喜欢标点符号!

C++向C语言添加了三个特殊运算符来专门支持成员指针。`::*`用于声明指针,而`->*`和`.*`用于调用所指向的函数。似乎花费了非同寻常的注意力来关注语言中一个晦涩且很少使用的部分。(甚至允许您重载`->*`运算符,但您为什么要这样做超出了我的理解。我只知道一个这样的用法[Meyers]。)

成员函数指针可以设置为0,并提供`==`和`!=`运算符,*但仅限于同一类的成员函数指针*。任何成员函数指针都可以与0进行比较,以检查它是否为`null`。[更新,2005年3月:这在所有编译器上都不起作用。在Metrowerks MWCC上,指向简单类第一个虚函数的指针将等于零!]与简单的函数指针不同,不等式比较(`<`、`>`、`<=`、`>=`)不可用。与函数指针一样,它们可以用作非类型模板参数,但这似乎在更少的编译器上有效。

关于成员函数指针的怪异之处

成员函数指针有些怪异之处。首先,您不能使用成员函数指针指向`static`成员函数。您必须为此使用普通函数指针。(因此,“成员函数指针”这个名字有点误导:它们实际上是“非静态成员函数指针”。)其次,在处理派生类时,有些令人惊讶。例如,如果您保留注释完好,下面的代码将在MSVC上编译:

class SomeClass {
 public: 
    virtual void some_member_func(int x, char *p) {
       printf("In SomeClass"); };
};

class DerivedClass : public SomeClass {
 public:
 // If you uncomment the next line, the code at line (*) will fail!
//    virtual void some_member_func(int x, char *p) { printf("In DerivedClass"); };
};

int main() {
    // Declare a member function pointer for SomeClass
    typedef void (SomeClass::*SomeClassMFP)(int, char *);
    SomeClassMFP my_memfunc_ptr;
    my_memfunc_ptr = &DerivedClass::some_member_func; // ---- line (*)
}

奇怪的是,`&DerivedClass::some_member_func`是`SomeClass`类的成员函数指针。它不是`DerivedClass`的成员!(某些编译器行为略有不同:例如,对于Digital Mars C++,在这种情况下`&DerivedClass::some_member_func`是未定义的。)但是,如果`DerivedClass`重写了`some_member_func`,代码将无法编译,因为`&DerivedClass::some_member_func`现在是`DerivedClass`类的成员函数指针!

成员函数指针之间的转换是一个极其模糊的领域。在C++标准化期间,关于您是否应该能够将一个类的成员函数指针转换为基类或派生类的成员函数指针,以及是否可以在不相关的类之间进行转换,进行了大量讨论。当标准委员会做出决定时,不同的编译器供应商已经做出了实现决策,这使他们对这些问题的答案不同。根据标准(第5.2.10/9节),您可以使用`reinterpret_cast`将一个类的成员函数存储在不相关类的成员函数指针中。调用强制转换的成员函数的结果是未定义的。您可以做的唯一事情就是将其强制转换回它所属的类。我将在本文后面详细讨论这一点,因为这是标准与实际编译器很少有相似之处的领域。

在某些编译器上,即使在基类和派生类之间的成员函数指针之间进行转换时,也会发生奇怪的事情。涉及多重继承时,使用`reinterpret_cast`从派生类转换为基类可能会或可能不会编译,具体取决于类在派生类声明中的列表顺序!这是一个例子:

class Derived: public Base1, public Base2  // case (a)
class Derived2: public Base2, public Base1 // case (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`时发出抗议,但原因不同。但是,某些编译器无论您做什么都会很高兴。请小心!

标准中还有另一个有趣的规则:您可以在类尚未定义之前声明成员函数指针。您甚至可以调用这个不完整类型的成员函数!这将在本文后面讨论。请注意,一些编译器无法处理此问题(早期MSVC、早期CodePlay、LVMM)。

值得注意的是,除了成员函数指针外,C++标准还提供了成员数据指针。它们共享相同的运算符,以及一些相同的实现问题。它们用于`stl::stable_sort`的某些实现中,但我不知道还有其他明智的用法。

成员函数指针的用途

到目前为止,我可能已经说服您成员函数指针有些古怪。但它们有什么用呢?我广泛搜索了网络上已发布的代码,以找出答案。我发现了成员函数指针的两种常见用法:

  1. 为了向C++新手演示语法的构造示例,以及
  2. 委托的实现!

它们在STL和Boost库中的单行函数适配器中也有微不足道的用途,允许您将成员函数与标准算法一起使用。在这种情况下,它们在编译时使用;通常,编译后的代码中实际上没有函数指针。成员函数指针最有趣的应用程序是定义复杂的接口。通过这种方式可以完成一些令人印象深刻的事情,但我没有找到很多例子。大多数时候,这些工作可以通过虚函数或重构问题更优雅地完成。但迄今为止,成员函数指针最著名的用途是在各种应用程序框架中。它们构成了MFC消息系统的核心。

当您使用MFC的消息映射宏(例如`ON_COMMAND`)时,您实际上是在填充一个包含消息ID和成员函数指针的数组(具体来说,是`CCmdTarget::*`成员函数指针)。这就是为什么MFC类必须从`CCmdTarget`派生才能处理消息。但是各种消息处理函数具有不同的参数列表(例如,`OnDraw`处理程序将`CDC*`作为第一个参数),因此数组必须包含各种类型的成员函数指针。MFC如何处理这个问题?他们使用了一个糟糕的技巧,将所有可能的成员函数指针放入一个巨大的联合体中,以破坏正常的C++类型检查。(请参阅*afximpl.h*和*cmdtarg.cpp*中的`MessageMapFunctions`联合体以获取详细信息。)因为MFC是一个非常重要的代码,实际上,所有C++编译器都支持这个技巧。

在我搜索中,除了编译时使用外,我找不到多少成员函数指针的良好使用示例。尽管它们很复杂,但它们并没有为语言增加太多东西。很难摆脱C++成员函数指针设计有缺陷的结论。

在撰写本文时,我有一个要点:*C++标准允许您在成员函数指针之间进行转换,但不允许您在转换后调用它们,这是荒谬的*。这有三个原因。首先,转换在许多流行的编译器上并不总是有效(因此,转换是标准的,但不是可移植的)。其次,在*所有*编译器上,如果转换成功,调用强制转换的成员函数指针的行为将与您期望的一样:无需将其归类为“未定义行为”。(调用是可移植的,但不是标准的!)第三,允许转换而不允许调用是完全无用的;但如果允许转换和调用,则可以轻松实现高效的委托,为语言带来巨大的好处。

为了说服您接受这个有争议的论点,请考虑一个仅包含以下代码的文件。这是合法的C++。

class SomeClass;

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

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

请注意,编译器必须生成汇编代码来调用成员函数指针,并且*不知道*类`SomeClass`的任何信息。显然,除非链接器进行*极其*复杂的高度优化,否则代码*必须*正确工作,无论类的实际定义如何。直接的结果是,您可以安全地调用从完全不同的类转换而来的成员函数指针。

为了解释我论点的另一半,即转换并不像标准所说的那样工作,我需要详细讨论编译器如何具体实现成员函数指针。这也有助于解释为什么关于使用成员函数指针的规则如此严格。准确的成员函数指针文档很难获得,错误信息也很普遍,所以我检查了大量编译器生成的汇编代码。是时候动手了。

成员函数指针 - 为什么如此复杂?

类的成员函数与标准C函数略有不同。除了声明的参数外,它还有一个隐藏的参数称为`this`,它指向类实例。根据编译器,`this`可能在内部被视为普通参数,或者受到特殊处理。(例如,在VC++中,`this`通常使用`ECX`寄存器传递)。`this`与普通参数根本不同。对于虚函数,它在*运行时*控制哪个函数被执行。即使成员函数本质上是一个真正的函数,在标准C++中也没有办法让普通函数像成员函数一样工作:没有`thiscall`关键字来使其使用正确的调用约定。成员函数来自火星,普通函数来自金星。

您可能会猜测“成员函数指针”就像普通函数指针一样,只是保存一个代码指针。您就错了。在几乎所有编译器上,成员函数指针都比函数指针大。最奇怪的是,在Visual C++中,成员函数指针的长度可能是4、8、12或16字节,具体取决于它关联的类的性质以及使用的编译器设置!成员函数指针比您预期的要复杂。但情况并非总是如此。

让我们回到20世纪80年代初。当原始C++编译器(CFront)最初开发时,它只有单重继承。当引入成员函数指针时,它们很简单:它们只是带有额外`this`参数作为第一个参数的函数指针。当涉及虚函数时,函数指针指向一小段“thunk”代码。(更新,2004年10月:在`comp.lang.c++.moderated`上的一次讨论表明,CFront实际上没有使用thunks,它的效率要低得多。但它*可以*使用此方法,并且假设它使用了此方法,可以使以下讨论更容易理解。)

CFront 2.0的发布粉碎了这个田园诗般的世界。它引入了模板和多重继承。多重继承的附带损害之一是成员函数指针的“阉割”。问题是,对于多重继承,直到调用时才知道要使用哪个`this`指针。例如,假设您定义了以下四个类:

class A {
 public:
       virtual int Afunc() { return 2; };
};

class B {
 public: 
      int Bfunc() { return 3; };
};

// C is a single inheritance class, derives only from A
class C: public A {
 public: 
     int Cfunc() { return 4; };
};

// D uses multiple inheritance
class D: public A, public B {
 public: 
    int Dfunc() { return 5; };
};

假设我们为类`C`创建一个成员函数指针。在这个例子中,`Afunc`和`Cfunc`都是`C`的成员函数,所以我们的成员函数指针可以指向`Afunc`或`Cfunc`。但是`Afunc`需要指向`C::A`的`this`指针(我称之为`Athis`),而`Cfunc`需要指向`C`的`this`指针(我称之为`Cthis`)。编译器编写者通过一个技巧来处理这种情况:他们确保`A`物理上存储在`C`的开头。这意味着`Athis == Cthis`。我们只需要担心一个`this`,一切都很好。

现在,假设我们为类`D`创建一个成员函数指针。在这种情况下,我们的成员函数指针可以指向`Afunc`、`Bfunc`或`Dfunc`。但是`Afunc`需要指向`D::A`的`this`指针,而`Bfunc`需要指向`D::B`的`this`指针。这次,这个技巧不起作用。我们不能将`A`*和*`B`都放在`D`的开头。所以,指向`D`的成员函数指针需要指定不仅要调用哪个函数,还要指定要使用哪个`this`指针。编译器知道`A`的大小,因此可以通过添加一个偏移量(`delta = sizeof(A)`)将`Athis`指针转换为`Bthis`。

如果您使用虚继承(即虚基类),情况会更糟,您很容易因尝试理解它而精神崩溃。通常,编译器使用虚函数表(“vtable”),该表为每个虚函数存储函数地址和`virtual_delta`:需要添加到提供的`this`指针上的字节数,以将其转换为函数所需的`this`指针。

如果C++对成员函数指针的定义稍有不同,就不会存在所有这些复杂性。在上面的代码中,复杂性仅仅来自于允许您将`A::Afunc`引用为`D::Afunc`。这可能是不好的风格;通常,您应该使用基类作为接口。如果您被迫这样做,那么成员函数指针可以是带有特殊调用约定的普通函数指针。依我看,允许它们指向被覆盖的函数是一个悲剧性的错误。作为对这种很少使用的额外功能的补偿,成员函数指针变得怪异。它们也给编译器编写者带来了头痛,他们不得不实现它们。

成员函数指针的实现

那么,编译器通常如何实现成员函数指针?以下是我们通过在32位、64位和16位编译器上对各种结构(`int`、`void*`数据指针、代码指针(即指向`static`函数的指针)以及指向具有单重、多重、虚继承或未知(即前向声明)的类的成员函数指针)应用`sizeof`运算符获得的一些结果。

编译器 选项 int 数据指针 代码指针 Single 多重 未知
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 16
Intel_IA32 /vmg /vmm 4 4 4 4 8 -- 8
Intel_Itanium   4 8 8 8 12 16 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
  medium 2 2 4 4 4 4 4
WCL small 2 2 2 6 6 6 6
  compact 2 4 2 6 6 6 6
  medium 2 2 4 8 8 8 8
  large 2 4 4 8 8 8 8

# 如果使用`__single`/`__multi`/`__virtual_inheritance`关键字,则为4、8或12。

编译器包括Microsoft Visual C++ 4.0到7.1(.NET 2003)、GNU G++ 3.2(MingW二进制文件,www.mingw.org)、Borland BCB 5.1(www.borland.com)、Open Watcom(WCL)1.2(www.openwatcom.org)、Digital Mars(DMC)8.38n(www.digitalmars.com)、Intel C++ 8.0 for Windows IA-32、Intel C++ 8.0 for Itanium(www.intel.com)、IBM XLC for AIX(Power, PowerPC)、Metrowerks Code Warrior 9.1 for Windows(www.metrowerks.com)和Comeau C++ 4.3(www.comeaucomputing.com)。Comeau的数据适用于其所有支持的32位平台(x86、Alpha、SPARC等)。16位编译器还在四种DOS配置(tiny、compact、medium和large)下进行了测试,以显示不同代码和数据指针大小的影响。MSVC还使用了选项(/vmg)进行了测试,该选项提供了“指针到成员的完全通用性”。(如果您有未在此处列出的供应商的编译器,请告知我。非x86处理器的结果尤其宝贵。)

令人惊叹,不是吗?查看此表,您可以轻易地看到编写在某些情况下有效但在其他情况下无法编译的代码是多么容易。内部实现显然在编译器之间差异很大;事实上,我认为没有其他语言特性具有如此多样化的实现。详细查看实现会揭示一些令人惊讶的棘手之处。

行为良好的编译器

对于几乎所有编译器,我称之为`delta`和`vindex`的两个字段用于调整提供的`this`指针,将其转换为传递给函数的`adjustedthis`。例如,以下是Watcom C++和Borland使用的技术:

struct BorlandMFP { // also used by Watcom
   CODEPTR m_func_address;
   int delta;
   int vindex; // or 0 if no virtual inheritance
};
if (vindex==0) adjustedthis = this + delta; 
else adjustedthis = *(this + vindex -1) + delta
CALL funcadr

如果使用虚函数,函数指针指向一个两指令的“thunk”来确定要调用的实际函数。Borland应用了一个优化:如果它知道类只使用了单重继承,它就知道`delta`和`vindex`将始终为零,因此它可以跳过最常见情况下的计算。重要的是,它只改变调用计算,而不改变结构本身。

许多其他编译器使用相同的计算,通常只是结构顺序的微小重新排列。

// Metrowerks CodeWarrior uses a slight variation of this theme.
// It uses this structure even in Embedded C++ mode, in which
// multiple inheritance is disabled!
struct MetrowerksMFP {
   int delta;
   int vindex; // or -1 if no virtual inheritance
   CODEPTR func_address;
};

// An early version of SunCC apparently used yet another ordering:
struct {
   int vindex; // or 0 if a non-virtual function
   CODEPTR func_address; // or 0 if a virtual function
   int delta;
};

Metrowerks似乎没有内联计算。相反,它在一个简短的“成员函数调用器”例程中执行。这稍微减小了代码大小,但使其成员函数指针稍慢。

Digital Mars C++(以前称为Zortech C++,然后是Symantec C++)使用不同的优化。对于单重继承类,成员函数指针只是函数的地址。当涉及更复杂的继承时,成员函数指针指向一个“thunk”函数,该函数对`this`指针进行必要的调整,然后调用实际的成员函数。这种小的thunk函数是为每个涉及多重继承的成员函数创建的。这无疑是我最喜欢的实现。

struct DigitalMarsMFP { // Why doesn't everyone else do it this way?
   CODEPTR func_address;
};

GNU编译器的当前版本使用一种奇怪而复杂的优化。它注意到,对于虚继承,您必须查找`vtable`才能获取计算`this`指针所需的`voffset`。在执行此操作时,您不妨将函数指针存储在vtable中。通过这样做,我们将`m_func_address`和`m_vtable_index`字段合并为一个,并通过确保函数指针始终指向偶数地址但`vtable`索引始终为奇数来区分它们。

// GNU g++ uses a tricky space optimisation, also adopted by IBM's VisualAge and XLC.
struct GnuMFP {
   union {
     CODEPTR funcadr;    // always even
     int vtable_index_2; //  = vindex*2+1, always odd
   };
   int delta;
};
adjustedthis = this + delta
if (funcadr & 1) CALL (* ( *delta + (vindex+1)/2) + 4)
else CALL funcadr

G++方法有很好的文档记录,因此已被许多其他供应商采用,包括IBM的VisualAge和XLC编译器、Open64的最新版本、Pathscale EKO和Metrowerks的64位编译器。GCC早期版本使用的一种更简单的方案也非常常见。SGI现已停产的MIPSPro和Pro64编译器,以及Apple古老的MrCpp编译器使用了这种方法。(请注意,Pro64编译器已成为开源的Open64编译器)。

struct Pro64MFP {
     short delta;
     short vindex;
     union {
       CODEPTR funcadr; // if vindex==-1
       short __delta2;
     } __funcadr_or_delta2;
   };
// If vindex==0, then it is a null pointer-to-member.

基于Edison Design Group前端的编译器(Comeau、Portland Group、Greenhills)使用一种几乎相同的方法。它们的计算是(PGI 32位编译器):

// Compilers using the EDG front-end (Comeau, Portland Group, Greenhills, etc)
struct EdisonMFP{
    short delta;
    short vindex;
    union {
     CODEPTR funcadr; // if vindex=0
     long vtordisp;   // if vindex!=0
    };
};
if (vindex==0) {
   adjustedthis=this + delta;
   CALL funcadr;  
} else { 
   adjustedthis = this+delta + *(*(this+delta+vtordisp) + vindex*8);
   CALL *(*(this+delta+funcadr)+vindex*8 + 4); 
};

大多数嵌入式系统的编译器不允许多重继承。因此,这些编译器避免了所有怪癖:成员函数指针只是一个带有隐藏“this”参数的普通函数指针。

微软“最小类”方法的肮脏故事

微软编译器使用一种类似于Borland的优化。它们以最佳效率处理单重继承。但与Borland不同的是,默认情况下会丢弃*始终*为零的条目。这意味着单重继承指针的大小与简单函数指针相同,多重继承指针更大,虚继承指针更大。这节省了空间。但它不符合标准,并且有一些奇怪的副作用。

首先,在派生类和基类之间转换成员函数指针可能会改变其大小!因此,信息可能会丢失。其次,当在类定义之前声明成员函数时,编译器必须计算要为其分配多少空间。但它无法可靠地做到这一点,因为它直到类定义完成后才知道类的继承性质。它必须猜测。如果它在一个源文件中猜测错误,但在另一个源文件中猜测正确,您的程序将在运行时莫名其妙地崩溃。因此,Microsoft为其编译器添加了一些保留字:`__single_inheritance`、`__multiple_inheritance`和`__virtual_inheritance`。他们还添加了一个编译器开关:*/vmg*,它通过保留缺失的零字段使所有MFP的大小相同。这时,故事变得肮脏起来。

文档暗示指定*/vmg*等同于用`__virtual_inheritance`关键字声明每个类。事实并非如此。相反,它使用了一个我称之为`unknown_inheritance`的更大的结构。当它必须为仅看到前向声明的类创建成员函数指针时,它也使用这个结构。他们无法使用`__virtual_inheritance`指针,因为它们使用了一个非常愚蠢的优化。以下是它们使用的算法:

// Microsoft and Intel use this for the 'Unknown' case.
// Microsoft also use it when the /vmg option is used
// In VC1.5 - VC6, this structure is broken! See below. 
struct MicrosoftUnknownMFP{
   FunctionPointer m_func_address; // 64 bits for Itanium.
   int m_delta;
   int m_vtordisp;
   int m_vtable_index; // or 0 if no virtual inheritance
};
 if (vindex=0) adjustedthis = this + delta
 else adjustedthis = this + delta + vtordisp + *(*(this + vtordisp) + vindex)
 CALL funcadr

在虚继承的情况下,`vtordisp`值*不*存储在`__virtual_inheritance`指针中!相反,编译器在调用函数时将其硬编码到汇编输出中。但为了处理不完整类型,您需要知道它。因此,它们最终得到了*两种*虚继承指针类型。但直到VC7,`unknown_inheritance`的情况才完全是错误的。`vtordisp`和`vindex`字段*始终*为零!可怕的后果:在VC4-VC6上,指定*/vmg*选项(无*/vmm*或*/vms*)可能导致调用错误的函数!这会非常难以追踪。在VC4中,IDE有一个用于选择*/vmg*选项的框,但它是禁用的。我怀疑MS的*某个人*知道这个bug,但它从未在其知识库中列出。他们终于在VC7中修复了它。

Intel使用与MSVC相同的计算,但其*/vmg*选项的行为却大不相同(它几乎没有影响——它只影响`unknown_inheritance`情况)。其编译器的发行说明指出,指针到成员类型的转换在虚继承情况下不受完全支持,并警告如果您尝试这样做,可能会导致编译器崩溃或代码生成不正确。这是语言中一个非常棘手的角落。

然后是CodePlay。Codeplay的VectorC的早期版本具有与Microsoft VC6、GNU和Metrowerks链接兼容性的选项。但是,它们始终使用Microsoft的方法。它们进行了逆向工程,就像我一样,但它们没有检测到`unknown_inheritance`情况或`vtordisp`值。它们的计算隐式地(且错误地)假设`vtordisp=0`,因此在某些(晦涩)情况下可能会调用错误的函数。但是Codeplay即将发布的VectorC 2.2.1已修复了这些问题。成员函数指针现在与Microsoft或GNU二者二进制兼容。通过一些强大的优化和标准的巨大改进(部分模板特化等),这正成为一个非常令人印象深刻的编译器。

我们从这一切中学到了什么?

理论上,所有这些供应商都可以彻底改变其表示MFP的技术。实际上,这极不可能,因为它会破坏大量现有代码。在MSDN上,有一篇微软发布的非常古老的文章解释了Visual C++的运行时实现细节[JanGray]。它由Jan Gray撰写,他于1990年实际编写了MS C++对象模型。尽管文章是1994年的,但仍然相关——除了bug修复,微软在15年里都没有改变它。同样,我拥有的最早的编译器(Borland C++ 3.0,(1990))生成的代码与Borland最近的编译器相同,当然16位寄存器被替换为32位寄存器。

到目前为止,您对成员函数指针的了解已经太多了。重点是什么?我带您走过这一切是为了确立一个规则。尽管这些实现彼此之间差异很大,但它们有一个有用的共同点:*调用成员函数指针所需的汇编代码是相同的,无论涉及什么类和参数*。一些编译器会根据类的继承性质应用优化,但当被调用的类是incomplete类型时,所有这些优化都是不可能的。这个事实可以被用来创建高效的委托。

委托(Delegates)

与成员函数指针不同,不难找到委托的用途。它们可以用于任何您会在C程序中使用函数指针的地方。也许最重要的是,使用委托可以非常容易地实现Subject/Observer设计模式[GoF,第293页]的改进版本。Observer模式最明显地适用于GUI代码,但我发现它在应用程序核心中能带来更大的好处。委托还可以优雅地实现Strategy[GoF,第315页]和State[GoF,第305页]模式。

现在,这是个丑闻。委托不仅比成员函数指针更有用。它们也更简单!由于委托由.NET语言提供,您可能会认为它们是一个高级概念,不容易在汇编代码中实现。事实恰恰相反:调用委托本质上是一个非常低级的概念,并且可以像普通函数调用一样低级(和快速)。C++委托只需要包含一个`this`指针和一个简单的函数指针。当您设置委托时,您会在指定要调用的函数的同时提供`this`指针。编译器可以在设置委托时计算出如何调整`this`指针。调用委托时无需执行任何操作。更好的是,编译器经常可以在编译时完成所有工作,因此即使设置委托也是一个微不足道的操作。在x86系统上调用委托生成的汇编代码*应该*像这样简单:

    mov ecx, [this]
    call [pfunc]

然而,在标准C++中没有办法生成如此高效的代码。Borland通过向其C++编译器添加新关键字(`__closure`)来解决此问题,允许它们使用方便的语法并生成最佳代码。GNU编译器还添加了一个语言扩展,但它与Borland的不兼容。如果您使用其中任何一个语言扩展,您就限制了自己只能使用一个编译器供应商。如果您相反,限制自己使用标准C++,仍然可以实现委托,只是效率不高。

有趣的是,在C#和其他.NET语言中,委托显然比函数调用慢几十倍(MSDN)。我怀疑这是因为垃圾回收和.NET安全要求。最近,Microsoft向Visual C++添加了一个“统一事件模型”,使用了`__event`、`__raise`、`__hook`、`__unhook`、`event_source`和`event_receiver`关键字。坦率地说,我认为这个特性很糟糕。它完全非标准,语法丑陋,甚至不像C++,并且生成的代码效率很低。

动机:对超快委托的需求

有大量的C++标准委托实现。它们都使用相同的想法。基本观察是成员函数指针充当委托——但它们只适用于单个类。为了避免这个限制,您添加了另一个间接层:您可以使用模板为每个类创建“成员函数调用器”。委托保存`this`指针,以及一个指向调用器的指针。成员函数调用器需要分配在堆上。

有许多此方案的实现,包括CodeProject上的几个。它们在复杂性、语法(尤其是与C#的相似性)和通用性方面有所不同。权威实现是boost::function。最近,它被采纳为下一版C++标准[Sutter1]。预计其使用将变得广泛。

尽管传统的实现非常聪明,但我发现它们不令人满意。尽管它们提供了所需的功能,但它们往往掩盖了根本问题:语言缺少一个低级构造。令人沮丧的是,在所有平台上,“成员函数调用器”代码对几乎所有类来说都是相同的。更重要的是,使用了堆。对于某些应用程序来说,这是不可接受的。

我的一个项目是离散事件模拟器。这种程序的_核心_是一个事件调度器,它调用被模拟的各种对象的成员函数。这些成员函数大多数非常简单:它们只更新对象的内部状态,有时将未来事件添加到事件队列中。这是使用委托的完美场景。然而,每个委托只被调用一次。最初,我使用了`boost::function`,但我发现委托的内存分配消耗了整个程序运行时间的超过三分之一!我想要真正的委托。老天,这应该只需要两个ASM指令!

我并不总是(经常?)得到我想要的,但这次我很幸运。我在这里提供的C++代码在几乎所有情况下都能生成最佳的汇编代码。最重要的是,*调用单目标委托的速度与普通函数调用一样快*。没有丝毫开销。唯一的缺点是,为了实现这一点,我不得不脱离标准C++的规则。我使用了成员函数指针的秘密知识来实现这一点。如果*非常*小心,并且不介意在某些情况下包含一些特定于编译器的代码,那么在任何C++编译器上都可以实现高效的委托。

技巧:将任意成员函数指针转换为标准形式

我的代码的核心是一个类,它将任意类指针和任意成员函数指针转换为泛型类指针和泛型成员函数。C++没有“泛型成员函数”类型,所以我将其转换为一个未定义的`CGenericClass`的成员函数。

大多数编译器对所有成员函数指针的处理方式都相同,无论类如何。对于大多数编译器,从给定的成员函数指针到泛型成员函数指针的直接`reinterpret_cast<>`都可以工作。事实上,如果这不起作用,那么编译器就是非标准的。对于其余的编译器(Microsoft Visual C++和Intel C++),我们必须将多重或虚继承成员函数指针转换为单重继承指针。这需要一些模板魔法和一个糟糕的技巧。请注意,该技巧仅在这些编译器不符合标准时才需要,但有一个很好的回报:该技巧可以生成最佳代码。

由于我们知道编译器在内部如何存储成员函数指针,并且知道在设置委托时需要如何调整`this`指针,我们可以在设置委托时自己调整`this`指针。对于单重继承指针,不需要调整;对于多重继承,有一个简单的加法;对于虚继承……这很混乱。但它有效,并且在大多数情况下,所有工作都在编译时完成!

我们如何区分不同的继承类型?没有官方的方法来确定一个类是否使用了多重继承。但是有一种偷偷摸摸的方法,如果您查看我之前提供的表格,就会发现——在MSVC上,每种继承风格都会产生不同大小的成员函数指针。所以,我们使用基于成员函数指针大小的模板特化!对于多重继承,这是一个微不足道的计算。对于未知继承(16字节)情况,使用了类似但更麻烦的计算。

对于Microsoft(和Intel)的糟糕、非标准的12字节`virtual_inheritance`指针,使用了另一个基于John Dlugosz提出的想法的技巧。Microsoft/Intel MFP的一个关键特性是,*无论其他成员的值如何*,`CODEPTR`成员*始终*被调用。(其他编译器并非如此,例如GCC,如果调用虚函数,则从vtable中获取函数地址。)Dlugosz的技巧是创建一个假的成员函数指针,其中`codeptr`指向一个返回所使用的“this”指针的探针函数。当您调用此函数时,编译器会为您完成所有计算工作,并利用秘密的`vtordisp`值。

一旦您能够将任何类指针和成员函数指针转换为标准形式,实现单目标委托就很容易(尽管繁琐)。您只需要为所有不同数量的参数创建模板类。

通过这种非标准转换实现委托的一个非常显著的额外好处是,它们可以进行相等比较。大多数现有的委托实现都无法做到这一点,这使得它们难以用于某些任务,例如实现多播委托[Sutter3]。

静态函数作为委托目标

理想情况下,一个简单的非成员函数,或一个`static`成员函数,可以作为委托目标。这可以通过将`static`函数转换为成员函数来实现。我能想到两种方法,在这两种方法中,委托都指向一个调用`static`函数的“调用器”成员函数。

“邪恶”方法使用了一个技巧。您可以存储函数指针而不是`this`指针,这样当调用调用器函数时,它只需要将`this`转换为`static`函数指针并调用它。这种方法的好处是它对普通成员函数的代码没有任何影响。问题在于它是一个技巧,因为它需要代码指针和数据指针之间的转换。它在代码指针比数据指针大的系统上(使用medium内存模型的DOS编译器)不起作用。它在我所知的*所有*32位和64位处理器上都有效。但因为它很“邪恶”,所以我们需要一个替代方案。

“安全”方法是将函数指针存储为委托的额外成员。委托指向其自身的成员函数。每当委托被复制时,这些自我引用必须被转换,这会使`=`和`==`运算符复杂化。这会使委托的大小增加四个字节,并增加代码的复杂性,但对调用速度没有影响。

我实现了这两种方法,因为它们都有优点:“安全”方法保证有效,“邪恶”方法生成编译器在支持委托的情况下可能生成的相同ASM代码。“邪恶”方法可以通过`#define`(`FASTDELEGATE_USESTATICFUNCTIONHACK`)启用。

脚注:为什么邪恶的方法有效?如果您仔细查看每个编译器调用成员函数指针时使用的算法,您会发现,对于所有这些编译器,当单重继承中使用非虚函数时(即`delta=vtordisp=vindex=0`),实例指针不会进入要调用哪个函数的计算。所以,即使`this`指针是垃圾,也会调用正确的函数。在该函数内部,接收到的`this`指针将是`garbage + delta = garbage`。(换句话说——垃圾进,*未修改*的垃圾出!)然后,我们可以将我们的垃圾转换回函数指针。如果“静态函数调用器”是虚函数,这种方法将不起作用。

Using the Code

随附的源代码是`FastDelegate`的实现(*FastDelegate.h*),以及一个演示*.*cpp文件来说明语法。要与MSVC一起使用,请创建一个空的控制台应用程序,并将这两个文件添加到其中。对于GNU,只需在命令行中键入“`g++ demo.cpp`”。

快速委托可以处理任何参数组合,但为了使其在尽可能多的编译器上工作,您必须在声明委托时指定参数数量。最多有八个参数,但增加此限制很容易。命名空间`fastdelegate`已使用。所有混乱都在名为`detail`的内部命名空间中。

`Fastdelegate`可以通过构造函数或`bind()`绑定到成员函数或静态(自由)函数。它们默认为0(`null`)。也可以使用`clear()`将其设置为`null`。可以使用运算符`!`或`empty()`测试它们是否为`null`。

与大多数其他委托实现不同,提供了相等运算符(`==`、`!=`)。即使涉及内联函数,它们也有效。

以下是*FastDelegateDemo.cpp*的一个摘录,显示了大多数允许的操作。`CBaseClass`是`CDerivedClass`的虚拟基类。可以轻松设计一个华丽的例子;这只是为了说明语法。

using namespace fastdelegate;

int main(void)
{
    // Delegates with up to 8 parameters are supported.
    // Here's the case for a void function.
    // We declare a delegate and attach it to SimpleVoidFunction()
    printf("-- FastDelegate demo --\nA no-parameter 
             delegate is declared using FastDelegate0\n\n");             
           
    FastDelegate0 noparameterdelegate(&SimpleVoidFunction);

    noparameterdelegate(); 
    // invoke the delegate - this calls SimpleVoidFunction()

    printf("\n-- Examples using two-parameter delegates (int, char *) --\n\n");

    typedef FastDelegate2<int, char *> MyDelegate;

    MyDelegate funclist[10]; // delegates are initialized to empty
    CBaseClass a("Base A");
    CBaseClass b("Base B");
    CDerivedClass d;
    CDerivedClass c;
    
  // Binding a simple member function
    funclist[0].bind(&a, &CBaseClass::SimpleMemberFunction);
  // You can also bind static (free) functions
    funclist[1].bind(&SimpleStaticFunction);
  // and static member functions
    funclist[2].bind(&CBaseClass::StaticMemberFunction);
  // and const member functions
    funclist[3].bind(&a, &CBaseClass::ConstMemberFunction);
  // and virtual member functions.
    funclist[4].bind(&b, &CBaseClass::SimpleVirtualFunction);

  // You can also use the = operator. For static functions,
  // a fastdelegate looks identical to a simple function pointer.
    funclist[5] = &CBaseClass::StaticMemberFunction;

  // The weird rule about the class of derived
  // member function pointers is avoided.
  // Note that as well as .bind(), you can also use the 
  // MakeDelegate() global function.
    funclist[6] = MakeDelegate(&d, &CBaseClass::SimpleVirtualFunction);

  // The worst case is an abstract virtual function of a 
  // virtually-derived class with at least one non-virtual base class.
  // This is a VERY obscure situation, which you're unlikely to encounter 
  // in the real world, but it's included as an extreme test.
    funclist[7].bind(&c, &CDerivedClass::TrickyVirtualFunction);
  // ...BUT in such cases you should be using the base class as an 
  // interface, anyway. The next line calls exactly the same function.
    funclist[8].bind(&c, &COtherClass::TrickyVirtualFunction);

  // You can also bind directly using the constructor
    MyDelegate dg(&b, &CBaseClass::SimpleVirtualFunction);

    char *msg = "Looking for equal delegate";
    for (int i=0; i<10; i++) {
        printf("%d :", i);
        // The ==, !=, <=,<,>, and >= operators are provided
        // Note that they work even for inline functions.
        if (funclist[i]==dg) { msg = "Found equal delegate"; };
        // There are several ways to test for an empty delegate
        // You can use if (funclist[i])
        // or          if (!funclist.empty())
        // or          if (funclist[i]!=0)
        // or          if (!!funclist[i])
        if (funclist[i]) {
            // Invocation generates optimal assembly code.
            funclist[i](i, msg);
        } else { 
            printf("Delegate is empty\n");
        };
    }
};

非void返回值

代码的1.3版本增加了处理非void返回值的能力。与`std::unary_function`一样,返回类型是*最后一个*参数。它默认为`void`,这保持了向后兼容性,也意味着最常见的情况保持简单。我想在*任何*平台上都不损失性能地做到这一点。对于MSVC6以外的所有编译器,这都很容易实现。VC6有两个重要的限制:

  1. 您不能将`void`作为默认模板参数。
  2. 您不能返回`void`。

我通过两个技巧解决了这个问题:

  1. 我创建了一个名为`DefaultVoid`的虚拟类。需要时将其转换为`void`。
  2. 每当需要返回`void`时,我就返回一个`const void*`。此类指针在`EAX`寄存器中返回。从编译器的角度来看,`void`函数和返回的返回值永远不会被使用的`void*`函数之间没有任何区别。最后的见解是认识到在调用函数中从`void`转换为`void*`是不可能不生成低效代码的。但是,如果您在接收函数指针的*那一刻*就转换它,那么所有工作都将在编译时完成(即,您需要转换函数的*定义*,而不是返回值本身)。

有一个破坏性更改:所有`FastDelegate0`实例必须更改为`FastDelegate0<>`。此更改可以通过对所有文件进行全局搜索和替换来执行,因此我认为它不应过于繁重。我相信这个更改使语法更直观:任何类型的`void FastDelegate`的声明现在都与函数声明相同,只是用`<>`替换了`()`。如果此更改真的惹恼了任何人,您可以修改头文件:将`FastDelegate0<>`定义包装在一个单独的`namespace newstyle {`和`} typedef newstyle::FastDelegate0<> FastDelegate0;`中。您还需要更改相应的`MakeDelegate`函数。

将FastDelegate作为函数参数传递

`MakeDelegate`模板允许您将`FastDelegate`用作函数指针的即插即用替换。一个典型的应用是将`FastDelegate`作为类的*私有*成员,并使用修改器函数来设置它(类似于Microsoft的`__event`)。例如:

// Accepts any function with a signature like: int func(double, double);
class A {
public:
    typedef FastDelegate2<double, double, int> FunctionA;
    void setFunction(FunctionA somefunc){ m_HiddenDelegate = somefunc; }
private:
    FunctionA m_HiddenDelegate; 
};
// To set the delegate, the syntax is:

A a;
a.setFunction( MakeDelegate(&someClass, &someMember) ); // for member functions, or
a.setFunction( &somefreefunction ); // for a non-class or static function

自然语法和Boost兼容性(1.4版新增)

Jody Hagins增强了`FastDelegateN`类,以允许提供与最新版本`Boost.Function`和`Boost.Signal`相同的吸引人语法。在支持部分模板特化的编译器上,您可以选择编写`FastDelegate< int (char *, double)>`而不是`FastDelegate2<char *, double, int>`。Jody做得太棒了!如果您的代码需要在VC6、VC7.0或Borland上编译,您需要使用旧的、可移植的语法。我已进行了更改,以确保旧语法和新语法100%等效,可以互换使用。

Jody还贡献了一个帮助函数`bind`,允许将为`Boost.Function`和`Boost.Bind`编写的代码快速转换为`FastDelegate`。这使您可以快速确定如果切换到`FastDelegate`,现有代码的性能将提高多少。它可以在“*FastDelegateBind.h*”中找到。如果我们有代码:

      using boost::bind;
      bind(&Foo:func, &foo, _1, _2);

我们应该能够将“`using`”替换为`using fastdelegate::bind`,一切都会正常工作。警告:`bind`的参数将被忽略!实际上没有执行绑定。行为仅在仅使用基本占位符参数`_1`、`_2`、`_3`等的最常见、最简单的(但最常见的)情况下等同于`boost::bind`。未来的版本可能会正确支持`boost::bind`。

有序比较运算符(1.4版新增)

同一类型的`FastDelegate`现在可以与`<`、`>`、`<=`、`>=`进行比较。成员函数指针不支持这些运算符,但它们可以用`memcmp()`进行简单的二进制比较来模拟。生成的严格弱序在物理上没有意义,并且依赖于编译器,但它允许它们存储在有序容器中,如`std:set`。

DelegateMemento类(1.4版新增)

提供了一个新类`DelegateMemento`,允许不同的委托集合。每个`FastDelegate`类都添加了两个额外的成员:

const DelegateMemento GetMemento() const;
void SetMemento(const DelegateMemento mem);

`DelegateMemento`可以相互复制和比较(`==`、`!=`、`>`、`<`、`>=`、`<=`),允许它们存储在任何有序或无序容器中。它们可以用作C中函数指针联合体的替代品。与不同类型的联合体一样,您有责任确保一致地使用同一类型。例如,如果您从`FastDelegate2`获取`DelegateMemento`并将其保存到`FastDelegate3`中,您的程序在调用它时很可能会崩溃。将来,我可能会添加一个调试模式,使用`typeid`运算符来强制类型安全。`DelegateMemento`主要用于其他库,而不是通用的用户代码。一个重要的用法是Windows消息处理,其中动态`std::map<MESSAGE, DelegateMemento>`可以替换MFC和WTL中的*静态*消息映射。但这又是另一篇文章了。

隐式转换为bool(1.5版新增)

您现在可以使用`if (dg) {...}`语法(其中`dg`是一个快速委托)作为`if (!dg.empty())`、`if (dg!=0)`甚至丑陋但高效的`if (!!dg)`的替代。如果您只是使用代码,您需要知道的是它在所有编译器上都能正常工作,并且不会无意中允许其他操作。

实现比预期的要困难。仅仅提供`operator bool`是危险的,因为它允许您编写类似`int a = dg;`这样的代码,而您可能的意思是`int a = dg();`。解决方案是使用Safe Bool idiom[Karlsson]:使用转换为*私有*成员数据指针而不是`bool`。不幸的是,通常的Safe Bool实现会搞乱`if (dg==0)`语法,并且一些编译器在成员数据指针的实现中存在bug(嗯,又一篇文章?),所以我必须开发几个技巧。过去其他人使用的一种方法是允许与整数进行比较,并在整数*非零*时*断言*。相反,我使用了一种更冗长的方法。唯一副作用是与函数指针的比较更优化!不支持与常量`0`的有序比较(但与碰巧是`null`的函数指针进行比较是有效的)。

许可证

本文附带的源代码已进入公共领域。您可以将其用于任何目的。坦率地说,写这篇文章的工作量大约是写代码的十倍。当然,如果您使用这些代码创建了伟大的软件,我很乐意听到。而且投稿总是受欢迎的。

可移植性

因为它依赖于标准未定义的行为,所以我小心翼翼地在许多编译器上测试了这些代码。讽刺的是,它比许多“标准”代码更具可移植性,因为大多数编译器并未完全符合标准。由于被广泛知晓,它也变得更安全了。主要的编译器供应商和C++标准委员会的几位成员都知道这里介绍的技术(在许多情况下,首席编译器开发人员已联系我讨论这篇文章)。供应商做出会不可逆转地破坏代码的更改的可能性微乎其微。例如,支持Microsoft的第一个64位编译器不需要任何更改。Codeplay甚至将`FastDelegates`用于其`VectorC`编译器的内部测试(这并非完全认可,但非常接近)。

`FastDelegate`实现在Windows、DOS、Solaris、BSD和几种Linux版本上进行了测试,使用了x86、AMD64、Itanium、SPARC、MIPS、.NET虚拟机以及一些嵌入式处理器。以下编译器已成功测试:

  • Microsoft Visual C++ 6.0、7.0(.NET)、7.1(.NET 2003)和8.0(2005)Beta(包括/clr“托管C++”)。
  • {已编译和链接,并检查了ASM列表,但未运行} Microsoft 8.0 Beta 2 for Itanium和AMD64。
  • GNU G++ 2.95、3.0、3.1、3.2和3.3(Linux、Solaris和Windows(MingW、DevCpp、Bloodshed))。
  • Borland C++ Builder 5.5.1和6.1。
  • Digital Mars C++ 8.38(x86,32位和16位,Windows和所有DOS内存模型)。
  • Intel C++ for Windows(x86)8.0和8.1。
  • Metrowerks CodeWarrior for Windows 9.1(C++和EC++模式)。
  • CodePlay VectorC 2.2.1(Windows、Playstation 2)。*不支持*早期版本。
  • Portland Group PGI Workstation 5.2 for Linux,32位。
  • {已编译,但未链接或运行} Comeau C++ 4.3(x86 NetBSD)。
  • {已编译和链接,并检查了ASM列表,但未运行} Intel C++ 8.0和8.1 for Itanium、Intel C++ 8.1 for EM64T/AMD64。

以下是据我所知仍在使用的所有其他C++编译器的状态:

  • Open Watcom WCL:一旦编译器添加了成员函数模板,它就能工作。核心代码(成员函数指针之间的转换)在WCL 1.2上有效。
  • LVMM:核心代码有效,但目前编译器存在过多bug。
  • IBM Visual Age和XLC:应该有效,因为IBM声称与GCC有100%二进制兼容性。
  • Pathscale EKO:应该有效,它也与GCC二进制兼容。
  • 所有使用EDG前端的编译器(GreenHills、Apogee、WindRiver等)也应该有效。
  • Paradigm C++:未知,但似乎只是早期Borland编译器的一个包装。
  • Sun C++:未知。
  • Compaq CXX:未知。
  • HP aCC:未知。

还有些人还在抱怨代码不具可移植性!(叹气)。

结论

本来只是解释我写的一小段代码,结果却变成了一个关于语言一个晦涩部分的恐怖教程。我还发现了六个流行编译器中以前未报告的bug和不兼容性。为了两行汇编代码,这真是付出了巨大的努力!

我希望我澄清了关于成员函数指针和委托晦涩世界的一些误解。我们已经看到,成员函数指针的许多怪异之处源于它们在不同编译器上的实现方式非常不同。我们还看到,与普遍看法相反,委托并不是复杂的高级构造,而是非常简单的。我希望我已经说服您,它们应该成为语言的一部分。当发布C++0x标准时,C++很可能添加某种形式的直接编译器支持来处理委托。(开始游说标准委员会!)

据我所知,没有哪个以前的C++委托实现像我在此介绍的FastDelegates那样高效或易于使用。您可能会觉得有趣的是,大部分代码是在我试图让我的小女儿入睡时,用一只手编程的……我希望您觉得它有用。

参考文献

  • [GoF]“设计模式:可复用面向对象软件的基础”,E. Gamma、R. Helm、R. Johnson和J. Vlissides。

在研究本文的过程中,我浏览了数十个网站。以下是一些最有趣的:

  • [Boost]。委托可以通过`boost::function`和`boost::bind`的组合来实现。`Boost::signals`是可用的最复杂的事件/消息传递系统之一。大多数Boost库都需要高度符合标准值的编译器。
  • [Loki]。Loki提供了“functor”,它们是可绑定参数的委托。它们与`boost::function`非常相似。Loki最终可能会合并到Boost中。
  • [Qt]。Qt 库包含一个信号/槽机制(即委托)。为了使其工作,您必须在编译之前对代码运行一个特殊的预处理器。性能很差,但它可以在模板支持非常差的编译器上工作。
  • [Libsigc++]。一个基于 Qt 的事件系统。它避免了 Qt 的特殊预处理器,但要求每个目标都必须派生自一个基对象类(使用虚拟继承——真糟糕!)。
  • [JanGray]。一篇 MSDN 的“幕后花絮”文章,描述了 Microsoft C/C++ 7 的对象模型。它仍然适用于所有后续的编译器版本。
  • [Hickey]。一个老的(1994 年)委托实现,它避免了内存分配。它假设所有成员函数指针的大小都相同,因此在 MSVC 上不起作用。这里有一个对代码有帮助的讨论 here
  • [Haendal]。一个专门讨论函数指针的网站?!不过关于成员函数指针的细节不多。
  • [Karlsson]。安全布尔模式。
  • [Meyers]。Scott Meyer 关于重载 operator ->* 的文章。请注意,经典的智能指针实现(Loki 和 boost)并不关心这个问题。
  • [Sutter1]。泛化函数指针:讨论了 boost::function 如何被接受到新的 C++ 标准中。
  • [Sutter2]。使用 std::tr1::function 泛化观察者模式(本质上是多播委托)。讨论了 boost::function 无法提供 == 运算符的局限性。
  • [Sutter3]。Herb Sutter 的“每周大师”文章,关于泛型回调。
  • [Dlugosz]。一个最近的委托/闭包实现,它和我的一样,效率极高,但只适用于 MSVC7 和 7.1。

历史

  • 1.0 - 2004 年 5 月 24 日。初始发布。(感谢 Arnold the Aardvaark 建议我将它们称为“委托”而不是“绑定的成员函数指针”)。
  • 1.1 - 2004 年 5 月 28 日。对代码进行了少量修改(主要是外观上的)。
    • 代码:防止了对邪恶的静态函数技巧的不安全使用。
    • 代码:改进了 horrible_cast 的语法(感谢 Paul Bludov)。
    • 代码:现在可以在 Metrowerks MWCC 和 Intel ICL (IA32) 上运行,并且可以在 Intel Itanium ICL 上编译。
  • 1.2 - 2004 年 6 月 27 日。在可移植性和健壮性方面有了重大改进。
    • 文章:添加了“微软方法的污秽史”+对 VC6 错误的讨论。
    • 文章:修正了关于 GCC 和 Intel 实现的错误。
    • 文章:大约二十处小修正和添加。
    • 代码:现在可以在 Borland C++ Builder 5.5 上运行。
    • 代码:现在可以在 VC7、VC7.1 上的 /clr “托管 C++”代码上运行。
    • 代码:Comeau C++ 现在可以无警告编译。
    • 代码:改进了警告和错误消息。非标准技巧现在有了编译时检查,使其更安全。
    • 代码:防止了虚拟继承在 VC6 及更早版本上生成错误代码的情况。
    • 代码:如果调用 const 成员函数,可以使用 const 类指针。
    • 代码:添加了 MakeDelegate() 全局辅助函数,以简化按值传递。
    • 代码:添加了 fastdelegate.clear()
  • 1.2.1 - 2004 年 7 月 16 日。小的错误修复。
    • 代码:针对 GCC 错误(模板中的 const 成员函数指针)的解决方法。
  • 1.3 - 2004 年 10 月 25 日。非 void 返回值以及对 Microsoft 编译器的无缝支持。整合了 Neville Franks、Ratheous 等人的建议。
    • 文章:修正了关于 CFront 1 中 MFP 表示的错误(感谢 Scott Meyers)。
    • 文章:添加了关于 EDG 成员指针表示的详细信息。
    • 文章:添加了关于返回值、许可证和 MakeDelegate 函数的部分。
    • 文章:对许多小问题进行了添加和澄清。
    • 代码:支持(非 void)返回值。
    • 代码:现在使用 John M. Dlugosz 发明的巧妙技巧,消除了对 FASTDELEGATEDECLARE 宏的需求以及 VC6 的问题。
    • 代码:制作了一个简单的程序(“Hopter”)来自动生成 FastDelegate.h 文件,减少了宏的使用。错误消息应该更容易理解。
    • 代码:添加了包含保护。
    • 代码:添加了 FastDelegate::empty() 来测试调用是否安全。
    • 代码:现在可以在 VS 2005 Express Beta、Portland Group C++ 上运行。
  • 1.4 - 2005 年 1 月 6 日。有序比较、DelegateMemento 类、函数声明符语法以及有限的 Boost 兼容性。整合了 Jody Hagins 的代码和 Rob Marsden 的建议。
    • 文章:对编译器实现部分进行了重大改写。添加了关于 CodePlay 的详细信息。
    • 文章:添加了描述新功能的章节。
    • 文章:对几乎所有部分的措辞都进行了改进,以减少歧义,并修正了小错误。
    • 代码:现在支持简单的 boost::function 风格语法。例如,FastDelegate< int (char *, double)>
    • 代码:添加了 DelegateMemento 类,允许集合不同委托。
    • 代码:添加了 ><>=<= 比较运算符,允许委托存储在有序集合中。
    • 代码:在 FastDelegateBind.h 中添加了 bind() 函数,以实现有限的 Boost 兼容性(感谢 Jody Hagins)。
    • 代码:现在可以在所有支持的编译器上无警告编译。
  • 1.4.1 - 2005 年 2 月 14 日。小的错误修复。
    • 文章:添加了关于 Open64、Pathscale EKO 和相关编译器所使用的实现的详细信息。
    • 代码:确认了与 MSVC-Itanium 和 MSVC-AMD64、ICL-EM64T 和 Open64-Itanium 的兼容性。(感谢 Stuart Dootson)。
    • 代码:错误修复:现在将空函数指针视为空委托,因此 dg==0 现在等同于 dg.empty(),而 dg=0 现在等同于 dg.clear()。(感谢 elfric)。
  • 1.5 - 2005 年 3 月 30 日。Safe_bool idiom 和完整的 CodePlay 支持。特别感谢 Jens-Uwe Dolinsky(CodePlay VectorC 前端的主要开发者)。
    • 文章:添加了“转换为 Bool”章节。
    • 文章:添加了指向俄语翻译的链接(感谢 Denis Bulichenko)。
    • 代码:确认 CodePlay VectorC 2.2.1 现在可以编译该代码。
    • 代码:错误修复:在 Metrowerks 上,empty() 有时可能为非空委托返回 true。(这是因为该编译器没有一个唯一的 null 成员函数指针值,违反了标准)。
    • 代码:现在支持 safe_bool idiom,因此 if (dg) 现在等同于 if (!dg.empty())。(这个功能是 Neville Franks 提出的,很久以前 :))。
    • 代码:已优化静态函数指针的赋值和比较。
© . All rights reserved.