更新记录:
2005-07-23 0:51: 根据 问题男 的指点作了修改,废弃了汇编代码,提高了移植性。新版本下载
一、前言
委托的重要性就不用再介绍了吧?C++标准没有实现委托,VC中实现的委托需要CLR支持,所以没有真正意义上的C++委托。
今天仔细看了i_like_cpp翻译的《成员函数指针与高性能的C++委托》,觉得实现过于复杂;又看了周星星的《类成员函数转化为普通函数(未完待续)(VC++6.0 & ASM)》,其中全以汇编来实现,没有实现出一个完整的delegate,似乎只是想验证thiscall的模拟。
前段时间我曾经实现过一个,不过在委托对象的成员函数时,要求类必须从某个基类派生(学MFC做的),虽然添加了析构时自动解除委托的功能,但前面这个要求却过于苛刻。
今天仔细想了一下,确实直接使用汇编来模拟thiscall调用是最快的,其实什么类型都可以抛弃,只要保证[1、ecx中存放对象地址;2、把参数压栈;3、调用(汇编的call)函数所在的地址]这3条即可,那么也就是说,我们可以把对象地址转为void*,成员函数指针转为void*,就可以用通用的方式存储了。调用时,如果对象地址为0则作为stdcall来调用,如果不为0,则模拟thiscall调用。
二、主要技术难点及说明
注:下面许多部分使用省略号,省略相似的内容。
1、由于是多分派委托,所以使用了一个vector <pair<void*, void*> >来存放对象-函数指针对或者普通函数,需要说明的是,我经过测试发现vector使用下标访问,效率远远高于使用迭代器,所以本文中vector的遍历都是使用下标方式。
2、由于参数个数有变化,经过考虑确定使用模板偏特化来实现,首先来看一下它的调用方式(模拟C#调用方式):
Delegate <void, int> f1; // 定义一个void delegate (int)委托
Delegate <void, int, const string&> f2; //定义一个void delegate (int, const string&)委托
要实现这样的调用方式,模板声明如下:
private:
NullType ();
};
template <typename Ret, typename A=NullType, typename B=NullType


class Delegate
{
private:
Delegate (); // 不可实例化
};
template <typename Ret>
class Delegate <Ret>
{
public:
Ret operator ( ) ( )
{

}
};
template <typename Ret, typename A>
class Delegate <Ret>
{
public:
Ret operator ( ) (A a )
{

}
};
3、其它要实现的还有:增加绑定(+=),清除绑定(=0),清除现有绑定并增加一个绑定(=func),本文为简化实现过程,暂时只实现了add函数即+=功能。add函数的有2个重载实现:

void add (func f); // 实现普通函数的绑定
template <typename T>
void (T* obj, Ret (T::*)(

4、由于C++语法的原因,不能直接使用Ret (T::*)(...)作为类型,所以这里增加了一个模板类Mem_Fun,用于提取类型信息。Mem_Fun类声明如下:
typename D=NullType, typename E=NullType, typename F=NullType, typename G=NullType,
typename H=NullType, typename I=NullType, typename J=NullType, typename K=NullType,
typename L=NullType, typename M=NullType, typename N=NullType, typename O=NullType,
typename P=NullType, typename Q=NullType, typename R=NullType, typename S=NullType,
typename T=NullType, typename U=NullType, typename V=NullType, typename W=NullType,
typename X=NullType, typename Y=NullType, typename Z=NullType
>
struct Mem_Fun
{
typedef Ret(Ty::*mem_fun0)();
typedef Ret(Ty::*mem_fun1)(A);
typedef Ret(Ty::*mem_fun2)(A,B);
typedef Ret(Ty::*mem_fun3)(A,B,C);

typedef Ret (*function0)();
typedef Ret (*function1)(A);
typedef Ret (*function2)(A,B);
typedef Ret (*function3)(A,B,C);

};
限于篇幅原因,这里省去若干行。(不过我真的定义了27个类型,是使用python脚本帮我输出的。)
使用这个模板类,上面3小节的add函数声明为:

template <typename T>
void add (T* obj, Mem_Fun<Ret, T,

由于add函数功能比较通用,所以单独写了一个Delegate_Base类,在Delegate::add中只是简单调用Delegate_Base::add。
5、前面已经讲过把成员函数指针转为void*存储,但C++编译器都不允许直接进行转型,尝试过使用union也无法编译,最后的解决办法是使用struct,如下:
Mem_Fun<Ret, T,

};
Pointer ptr = {f};
void* p = *(void**)&ptr;
呵呵,转过来了。
6、thiscall的模拟,这个在i_like_cpp和周星星的blog中都可以看到,其它相关的资料也比较多,其实就是增加一个汇编指令:mov ecx ptr,然后调用call mem_fun即可,当然要记得把参数压栈。这里摘录2段代码:
代码1(无参数委托偏特化的()函数):
void operator ( ) ( )
{
for (size_t i=0; i<_handlers.size (); i++)
{
void* p = _handlers[i].first;
if (p) // member function
{
void* mem_fun = _handlers[i].second;
__asm{
mov ecx, p
call mem_fun
}
}
else
{
(*(func)_handlers[i].second)();
}
}
}
代码2(有两个参数委托偏特化的()函数):
void operator ( ) (A a, B b)
{
for (size_t i=0; i<_handlers.size (); i++)
{
void* p = _handlers[i].first;
if (p) // member function
{
void* mem_fun = _handlers[i].second;
__asm{
push b
push a
mov ecx, p
call mem_fun
}
}
else
{
(*(func)_handlers[i].second)(a, b);
}
}
}
上面要注意的是参数压栈顺序。
7、由于多分派委托的返回值处理起来比较麻烦(i_like_cpp的译文里已经有说明),所以这里暂时没有做返回值。当然这不是什么麻烦事,有返回值和无返回值也可以通过偏特化来实现,没有去实现它的原因是,还没有确定在多分派情况下如何去处理。
8、[新加入]根据 问题男 的指点,修改成模拟thiscall调用,这里简单分析一下。
假定有void test3 (a, b, c)和void Test::test3 (a, b, c)这2个函数[注:这里不去考虑参数类型,Test::test3为nonstatic member function],void* p和void* f分别保存对象和函数指针,arg1, arg2, arg3是调用时的3个参数。先看一下调用过程:
调用test3:
void* f = (void*)test3;
// 调用
__asm{
push arg3
push arg2
push arg1
call f
add esp, 12
}
调用Test::test3:
typedef void (Test::*test_mem_fun)();
struct Ptr{test_mem_fun f;};
Ptr ptr = {&Test::test0};
void* p = (void*)&test;
void* f = *(void**)&ptr;
// 调用
__asm{
push c
push b
push a
mov ecx, p
call f
}
这个版本移植性不好,如果能让编译器帮我们产生调用代码,那是再好不过了。下面是让编译器帮我们生成调用,移植性也比较好。cdecl比较容易模拟,这里的thiscall是根据 问题男 的指点写成的。
调用test3:
void* f = (void*)test3;
// 调用。假设int, string, float是3个参数的类型
typedef void (*test3_func)(int, string, float);
(*(test3_func)f)(a, b, c);
调用Test::test3:
typedef void (Test::*test_mem_fun)();
struct Ptr{test_mem_fun f;};
Ptr ptr = {&Test::test0};
void* p = (void*)&test;
void* f = *(void**)&ptr;
// 调用
typedef void (NullType::*mem_fun3)(int, string, float);
mem_fun3 mf;
*(void**)&mf = f;
(((NullType*)p)->*f)(a, b, c);
注:开始我还有点怀疑这里是不是可能存在虚函数调用的问题,后经测试证实没有这问题,我想是因为取成员函数指针时,编译器已经做了处理,而实际调用时根本不需要处理。(纯属个人观点,有大虾帮我证实一下?)
三、实现过程。
基本上就是个编写、测试、比较、拷贝修改这样一个无聊的过程了,呵呵,不说了。实际上我也没有实现完整,目前只实现了3个参数的支持,修改代码是个麻烦事,我想等确定下来了,再把其它的偏特化实现补全。
四、代码下载。
Demo.rar (包括Deleagete.h和Demo.cpp)
五、效率。
初步测试速度比较满意,最初的版本有些慢,是因为我使用迭代器的缘故,改为下标方式就快了十倍以上。就我测试的结果来看,使用这个委托类并没有比直接调用慢,可能只是不明显吧,不过测试效率也有些麻烦,有时甚至使用委托类比直接调用还快,但这是不可能的。所以我只能总结一点,那就是使用这个类并不比直接调用慢多少。。。。。。嗨嗨。。我想可能是因为在表格中定位所损失的效率,比起函数调用的开销来说显得微不足道吧,这和虚函数调用差不多,我记得有人说过虚函数调用平均会损失5%的效率,当然也和代码块长度有关。