深圳夜归人

繁华的都市,有谁记得我们的脚步?

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

更新记录:
2005-07-23 0:51:  根据 问题男 的指点作了修改,废弃了汇编代码,提高了移植性。新版本下载

一、前言

委托的重要性就不用再介绍了吧?C++标准没有实现委托,VC中实现的委托需要CLR支持,所以没有真正意义上的C++委托。

今天仔细看了i_like_cpp翻译的《成员函数指针与高性能的C++委托》,觉得实现过于复杂;又看了周星星的《类成员函数转化为普通函数(未完待续)(VC++6.0 & ASM》,其中全以汇编来实现,没有实现出一个完整的delegate,似乎只是想验证thiscall的模拟。

前段时间我曾经实现过一个,不过在委托对象的成员函数时,要求类必须从某个基类派生(MFC做的),虽然添加了析构时自动解除委托的功能,但前面这个要求却过于苛刻。

今天仔细想了一下,确实直接使用汇编来模拟thiscall调用是最快的,其实什么类型都可以抛弃,只要保证[1ecx中存放对象地址;2、把参数压栈;3、调用(汇编的call)函数所在的地址]3条即可,那么也就是说,我们可以把对象地址转为void*,成员函数指针转为void*,就可以用通用的方式存储了。调用时,如果对象地址为0则作为stdcall来调用,如果不为0,则模拟thiscall调用。

二、主要技术难点及说明

 

注:下面许多部分使用省略号,省略相似的内容。

1
、由于是多分派委托,所以使用了一个vector <pair<void*, void*> >来存放对象-函数指针对或者普通函数,需要说明的是,我经过测试发现vector使用下标访问,效率远远高于使用迭代器,所以本文中vector的遍历都是使用下标方式。

2
、由于参数个数有变化,经过考虑确定使用模板偏特化来实现,首先来看一下它的调用方式(模拟C#调用方式)

 

Delegate <void> f0;    // 定义一个void delegate ()委托
Delegate <voidint> f1;  // 定义一个void delegate (int)委托
Delegate <voidintconst string&> f2;    //定义一个void delegate (int, const string&)委托

 

要实现这样的调用方式,模板声明如下:

 

class NullType{ // 一个无法实例化的类,用作默认参数
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个重载实现:


typedef Ret(*func)();
void add (func f);  // 实现普通函数的绑定

template 
<typename T>
void (T* obj, Ret (T::*)() f); // 实现成员函数绑定。[注:由于语法的原因,这里使用了特殊方式,见第4小节]

 

4、由于C++语法的原因,不能直接使用Ret (T::*)(...)作为类型,所以这里增加了一个模板类Mem_Fun,用于提取类型信息。Mem_Fun类声明如下:

template <typename Ret, typename Ty, typename A=NullType, typename B=NullType, typename C=NullType, 
    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函数声明为:

void add (Mem_Fun<Ret, NullType, >::functionN f);  // 实现普通函数的绑定

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,如下:

struct Pointer{
    Mem_Fun
<Ret, T, >::mem_funN p;
};
Pointer ptr 
= {f}; 
void* p = *(void**)&ptr;


呵呵,转过来了。

 

6thiscall的模拟,这个在i_like_cpp和周星星的blog中都可以看到,其它相关的资料也比较多,其实就是增加一个汇编指令:mov ecx ptr,然后调用call mem_fun即可,当然要记得把参数压栈。这里摘录2段代码:

 

代码1(无参数委托偏特化的()函数)

typedef typename Mem_Fun<void, NullType>::function0 func;

    
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(有两个参数委托偏特化的()函数)

 

typedef typename Mem_Fun<void, NullType, A, B>::function2 func;

    
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* p = (void*)0;
void* f = (void*)test3;
// 调用
__asm{ 
    push arg3
    push arg2 
    push arg1
    call f 
    add esp, 
12
}


调用Test::test3:

Test test;
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* p = (void*)0;
void* f = (void*)test3;
// 调用。假设int, string, float是3个参数的类型
typedef void (*test3_func)(intstringfloat);
(
*(test3_func)f)(a, b, c);


调用Test::test3:

Test test;
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)(intstringfloat);
mem_fun3 mf;
*(void**)&mf = f;
(((NullType
*)p)->*f)(a, b, c);


注:开始我还有点怀疑这里是不是可能存在虚函数调用的问题,后经测试证实没有这问题,我想是因为取成员函数指针时,编译器已经做了处理,而实际调用时根本不需要处理。(纯属个人观点,有大虾帮我证实一下?)

三、实现过程。

基本上就是个编写、测试、比较、拷贝修改这样一个无聊的过程了,呵呵,不说了。实际上我也没有实现完整,目前只实现了
3个参数的支持,修改代码是个麻烦事,我想等确定下来了,再把其它的偏特化实现补全。

 

四、代码下载。

 

Demo.rar (包括Deleagete.hDemo.cpp)

五、效率。

初步测试速度比较满意,最初的版本有些慢,是因为我使用迭代器的缘故,改为下标方式就快了十倍以上。就我测试的结果来看,使用这个委托类并没有比直接调用慢,可能只是不明显吧,不过测试效率也有些麻烦,有时甚至使用委托类比直接调用还快,但这是不可能的。所以我只能总结一点,那就是使用这个类并不比直接调用慢多少。。。。。。嗨嗨。。我想可能是因为在表格中定位所损失的效率,比起函数调用的开销来说显得微不足道吧,这和虚函数调用差不多,我记得有人说过虚函数调用平均会损失5%的效率,当然也和代码块长度有关。

posted on 2005-07-23 00:30  cpunion  阅读(1892)  评论(7编辑  收藏  举报