更新记录:
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 调用。
二、主要技术难点及说明
<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
注:下面许多部分使用省略号,省略相似的内容。
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:: * )() f); // 实现成员函数绑定。[注:由于语法的原因,这里使用了特殊方式,见第4小节]
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, > mem_funN f); // 实现成员函数绑定。
由于add函数功能比较通用,所以单独写了一个Delegate_Base类,在Delegate::add中只是简单调用Delegate_Base::add。
5、前面已经讲过把成员函数指针转为void*存储,但C++编译器都不允许直接进行转型,尝试过使用union也无法编译,最后的解决办法是使用struct,如下:
Mem_Fun < Ret, T, > ::mem_funN p;
};
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%的效率,当然也和代码块长度有关。