C++模板实现事件处理器中的“通用成员函数指针”的调用(一)

来源:互联网 发布:编程好看的字体 编辑:程序博客网 时间:2024/06/10 06:35
我最近在实现一个事件处理器,应用到了一种“通用成员函数指针的注册”,先声明,这个名词是我给起的,不过我觉得并不充分恰当,但也想不出什么更好的词,看完下面的介绍,也考考您,看这玩意叫个啥比较合适。

先说需求:这个事件处理器需要在捕获一个事件后,调用已注册的处理函数。看起来很普通,呵呵,不过,这个事件处理函数不一定是哪个类的成员函数(但肯定是成员函数),函数的形式是一定的,即拥有相同的返回类型和参数列表。而这些事件和处理函数,注册在一个全局的结构体中。

问题的提出:同一个结构体的各字段类型是固定的,所以仔细想一下,这个保存着事件和处理函数的结构体应该怎么定义呢?事件的类型是一定的,但是处理函数呢?需求中明确指出,这个处理函数很有可能是不同类中的成员函数,而成员函数指针在定义时,是需要明确类的,比如:
void (MyClass::*func_ptr)(void);

定义了一个指向MyClass类中的无参数、返回类型为void的成员函数指针。这样看来,我没有办法定义一个适用于任何类成员函数的指针呀?

那么,我能不能定义一个基类 HandlerBase,让所有包含事件处理函数的类都继承自HandlerBase呢?这样我可以在定义结构体时,将事件处理函数指针定义为
void (HandlerBase::*func_ptr)(void);
刚刚有这个想法时,我还禁不住一阵欣喜,但细想才发现,这也是行不通的,因为这样的话,我们必需将所有子类中用到的事件处理函数在HandlerBase中声明为虚函数,这样一来,代码就显得混乱没有章法了,所以,这个方法还是 pass 了吧。

看上去问题可能真的很复杂,那怎么办泥?仔细研究后,感觉只有用模板来解决问题了,具体怎么来设计呢?

(补充:当我写完下面一段话再回读的时候,发现我的表达可能并不是太好懂,那么请您硬着头皮看到最后,我相信您一定能明白我的意思的)

首先,通过上面的分析,看来直接将事件处理函数注册到结构体中是不行的,因为同一个结构体,不可能包含一种以上类型的成员。那么,就让我们把这些类型的成员做个统一处理:让我们声明一个基类(FuncCaller),然后把不同的处理函数注册到它的子类(FuncItem)中去,再通过基类中提供的方法来间接调用处理函数!

不过,我可不想针对每一个可能出现事件处理函数的类,都声明一个对应的 FuncItem(因为他们包含的成员函数指针类型不同),那好,现在到了模板上场的时候了,上代码:
复制代码
class FuncCaller
{
//

};

template 
<typename T>
class FuncItem : public FuncCaller
{
//

};
复制代码

有了这样的定义,我就可以在FuncItem中添加一个指针类型,并定义一个成员变量:

typedef void (T::*HandlerPtr)(void* param);
HandlerPtr handler;


param是事件到达时,可能传递给处理函数的数据。现在,我们在基类FuncCaller中,定义一个函数,来间接调用事件处理函数:

virtual void func_call(void* obj_ptr, void* param)=0;


之所以为纯虚函数,是因为基类是没有实现它的意义的。另外,形参 obj_ptr 用来传递调用事件处理函数的对象,因为成员函数指针是需要通过类的实例来调用的。
下面来看看FuncItem类对 func_call 的实现:

复制代码
void func_call(void* obj_ptr, void* param)
{
    
if!obj_ptr )
        
return ;

    (((T
*)obj_ptr)->*handler)(param);
}
复制代码


差不多了,来看看现在这两个类长成什么样了:

复制代码
 1class FuncCaller
 2{
 3public:
 4    virtual void func_call(void* obj_ptr, void* param)=0;
 5
 6}
;
 7
 8template <typename T>
 9class FuncItem : public FuncCaller
10{
11private:
12    typedef void (T::*HandlerPtr)(void* param);
13    HandlerPtr handler;
14
15public:
16    void func_call(void* obj_ptr, void* param)
17    {
18        if!obj_ptr )
19            return ;
20
21        (((T*)obj_ptr)->*handler)(param);
22    }

23
24}
;
复制代码

再来看看具体事件处理结构的设计,您有没有发现FuncItem有点问题吗?谁来给成员handler赋值呢?交给构造函数吧,改造后的FuncItem如下:


复制代码
template <typename T>
class FuncItem : public FuncCaller
{
private:
    typedef 
void (T::*HandlerPtr)(void* param);
    
const HandlerPtr handler;

public:
    
void func_call(void* obj_ptr, void* param)
    {
        
if!obj_ptr )
            
return ;

        (((T
*)obj_ptr)->*handler)(param);
    }

    FuncItem(
const HandlerPtr ptr):handler(ptr) {}  // 此处为新添加代码

};
复制代码


现在我们已经写好了 FuncCaller 和它的子类了,开始定义我们的结构体吧!这个结构体应该包含的信息,应该有事件及对应的处理函数,不过,处理函数我已经可以用 FuncCaller 来代替了,所以,具体定义如下:

typedef struct _EventRegister
{
    EventType 
event;
    FuncCaller
* func_caller;
} EventRegister;


至此,大功告成!为了测试我们的成果,让我们做一些工作吧:
1、写两个包含事件处理函数的类:

复制代码
class ClassA
{
public:
    
void show_me(void* param)
    { printf(
"I'm ClassA, show_me is called! param is 0x%x\n", param); }
};

class ClassB
{
public:
    
void come_on(void* param)
    { printf(
"I'm ClassB, come_on is called! param is 0x%x", param); }
};
复制代码


2、类有了,再来声明一个注册事件的结构体:

EventRegister event_center[] = {
    { EVENT_TEST_1  , 
new FuncItem<ClassA>(&ClassA::show_me) },
    { EVENT_TEST_2  , 
new FuncItem<ClassB>(&ClassB::come_on) },
    { EVENT_INVALID , NULL }
};


最后一项用来标识事件注册的结束。

3、写一个触发事件的函数:

复制代码
/**
 * Function:send_event
 * params:
 *        event -> event type
 *        target -> who will process the event
 
*/
void send_event(EventType eventvoid* target, void* param)
{
    
if!target )
        
return ;

    
int loop_event = 0;
    EventType et 
= event_center[loop_event].event;
    
while( et!=EVENT_INVALID )
    {
        
if( et==event )
    {
        event_center[loop_event].func_caller
->func_call(target, param);
        
break;
    }

    et 
= event_center[++loop_event].event;
    }
}
复制代码

 

现在,来写个main吧!

复制代码
void main()
{
    ClassA ca;
    ClassB cb;

    send_event(EVENT_TEST_1, (
void*)&ca, (void*)0xff00);
    send_event(EVENT_TEST_2, (
void*)&cb, NULL);
}
复制代码


好,运行之前我还得说一下,估计大家也都看到了,我在结构体中注册的事件处理器,与main中调用 send_event时传入的类实例很可能不属于同一个类,比如我在send_event调用时,将cb的指针传递给了事件EVENT_TEST_1,而不是ca的指针,这样会导致不可预期的运行结果。不过,幸好我们正在写的只是测试用的代码,而实际中应用的事件处理调度程序,要比这复杂得多,而且,具体传递给哪个实例来处理这个事件,是由调度程序来判断的,所以,一个好的调试程序的设计,也是至关重要的。不过,就算有再好的设计,我们也不能忽略代码中存在的风险。其实,我们眼前的这个风险,是有解决办法的,不过,先要行看看正常的运行结果:

I'm ClassA, show_me is called! param is 0xff00
I'm ClassB, come_on is called! param is 0x0


相当理想。别闲着,试试我们刚说的那个“风险”存在的真实性吧!
稍稍修改一个我们的ClassA:

复制代码
class ClassA
{
private:
    
char* name;

public:
    
void show_me(void* param)
    { printf(
"I'm ClassA, show_me is called! name is %s\n", name); }

    ClassA():name(
"ClassA") {}

};
复制代码


好,先编译运行一下,看看效果:

I'm ClassA, show_me is called! name is ClassA
I'm ClassB, come_on is called! param is 0x0


没问题,现在让我们检验风险的存在,修改 main 中的第一个 send_event:
send_event(EVENT_TEST_1, /*(void*)&ca*/(void*)&cb, (void*)0xff00);  // Note: 'ca' is replaced by 'cb'

再编译运行一下:

Segmentation fault


哈哈,正是我们预想的,原因说起来又一百字,我就省略了,但是效果是明显的。



首先,我们来分析一下风险形成的原因:
Step 1. 声明类A的成员函数指针,并让它指向类A的一个成员函数
Step 2. 将类B实例的指针强制转化为类A的指针,并通过它来调用类A的成员函数指针。因为类A与类B的结构是不一样的,所以这样做的结果也是不可预期的

那么,最好在Step2中,能够检测到指针的实际类型,并与类A的类型进行对比,不一至的话,就不再调用成员函数指针了。说到这,估计大家一定想到C++中的RTTI了,即运行时类型识别。而RTTI也可以通过两种方式来实现,一种是使用关键字typeid,还有一种就是使用 dynamic_cast。

简单说一下typeid,这个关键字可以返回一个常量的引用,我们可以对两个对象执行typeid操作,并通过判断其返回值是否相等,来检查两个对象是否是同一类型。举个例子:


ClassA ca;
ClassB cb;
if( typeid(ca)==typeid(cb) )
    


不过,在使用typeid时需要包含typeinfo头文件,而且,在gcc里,RTTI是有开关的,不过默认为开启,如果你想关掉它,可以加上 -fno-rtti选项。
typeid的另一个功能,是可以通过name操作来得到类型名称:

printf("ca is a %s\n", typeid(ca).name() );

不过,name操作返回的字符串,并不一定就是你在代码中定义的类型名称,具体和编译器有关。

但是,typeid是非常严格的,即属于父子关系的两个类,通过typeid操作得到的返回值是不同的。比如,若ClassB继承自ClassA,那么:

ClassA ca;
ClassB cb;
if( typeid(ca)==typeid(cb) )
    
// equal
else
    
// not equal


这个判断的结果是 not equal。

再来简单说一下 dynamic_cast,C++中的这个关键字用来提供更安全的运行时类型转换。还是上面的实例,如果ClassB继承自ClassA,那么:

extern ClassB *pcb;
ClassA 
*pca = dynamic_cast<ClassB*>(pcb);


这个实例,pcb是可以成功转化为pca的,但是,下面再添加一个与ClassA没有继承关系的ClassC,那么:

extern ClassC *pcc;
ClassA 
*pca = dynamic_cast<ClassC*>(pcc);


这时,pcc是不可以转化 为pca的,此时pca的值为NULL。还需要说明的是,如果 dynamic_cast将要转换的是一个指针,那么失败后会返回NULL;如果将要交换的是一个引用,那么失败后会抛出一个异常。

由此看来,dynamic_cast比typeid要智能许多了,但是它是以牺牲更多的系统开销做为代价的。


言规正传,在我们的事件处理器中,我准备使用typeid来做为类型判断的工具。不过,在实现这个想法时,还遇到了一个小波折。

回忆一下我们相关类的定义:

复制代码
 1class FuncCaller
 2{
 3public:
 4    virtual void func_call(void* obj_ptr, void* param)=0;
 5
 6}
;
 7
 8template <typename T>
 9class FuncItem : public FuncCaller
10{
11private:
12    typedef void (T::*HandlerPtr)(void* param);
13    const HandlerPtr handler;
14
15public:
16    void func_call(void* obj_ptr, void* param)
17    {
18        if!obj_ptr )
19            return ;
20
21        (((T*)obj_ptr)->*handler)(param);
22    }

23
24    FuncItem(const HandlerPtr ptr):handler(ptr) {}
25
26}
;
复制代码


我本想在 21 行处增加对类型的判断,即:

复制代码
void func_call(void* obj_ptr, void* param)
{
    
if!obj_ptr )
        
return ;

    
if( typeid(*obj_ptr)==typeid(T) )
        (((T
*)obj_ptr)->*handler)(param);
    
else
        printf(
"runtime error:get %s while expecting %s\n", typeid(*obj_ptr).name(), typeid(T).name() );
}
复制代码


可以在用g++翻译的时候却被告知:
'void*' is not a pointer-to-object type
看来 void* 是一种无类型的指针,是不允许对其指向的对象做 typeid 操作的。

在一翻冥思苦想后,除了定义一个基类,再也没有找到一个更好的解决办法。假设基类名称为 HandleBase,那么,我们可以让所以包含事件处理函数的类都继承自HandleBase,并对func_call做如下修改:

复制代码
void func_call(HandleBase* obj_ptr, void* param)
{
    
if!obj_ptr )
        
return ;

    
if( typeid(*obj_ptr)==typeid(T) )
        (((T
*)obj_ptr)->*handler)(param);
    
else
        printf(
"runtime error:get %s while expecting %s\n", typeid(*obj_ptr).name(), typeid(T).name() );
}
复制代码


这样做就绝对没有问题了。话说回来,我之所以不想为包含事件处理函数的类定义一个基类,就是不想增加类定义的复杂性,同时也可以提高事件处理器的通用性,但是,现在为了实现RTTI,却不得不牺牲这个原则了。不过还好,幸好只是继承一个基类而已。

既然增加一个基类已成定局,那么,就让我们好好利用这个基类,来做更多的事情。为了弥补前面说到的typeid中name操作可能不会返回原始类名,那么就让我们在基类里来记录一份原始类名吧,看看类 HandleBase 的定义:

复制代码
class HandleBase
{
public:
    
virtual const char* _object_real_type() = 0;
protected:
    
static const char* check_HandleBase_derived(const char* name) { return name; };
};
复制代码


奇怪,为什么不把名称定义成一个成员变量,而要定义成一个纯虚函数呢?还有check_HandleBase_derived,这又是做什么的?别急,我们再来定义一个宏:

#define Registe_Handle_Class(x) \
public: \
    
const char* _object_real_type() { return x::check_HandleBase_derived(#x); } \
    
static const char* _st_object_real_type() { return x::check_HandleBase_derived(#x); }


呵呵,别怨我卖关子,坚持看完下面ClassA的定义,您一定能明白:

class ClassA : public HandleBase
{
    Registe_Handle_Class(ClassA)
    
    
//
};


完成了!呵呵,怎么样,明白了吗?让我来解释上面的一连串动作吧,我准备采用倒序法来说明。先看ClassA的定义,它继承自HandleBase,原因刚才已经就说过了,至于在类声明的起始处调用宏Registe_Handle_Class,则是对相关操作的一个简化。那么,这个宏都做了哪些操作呢?再来看宏的定义,这时您会发现,这个宏其实就是实现我刚才所说的得到原始类名称的功能。宏的实现中,自动定义了基类HandleBase中的_object_real_type函数,使得外界可以通过_object_real_type调用得到对象的原始类名称。这也就是我为什么不定义一个存储类名的成员变量,而要定义一个纯虚函数的原因:定义成变量,是没办法在派生类的声明中对变量进行赋值的,这样的话,类的实现者就必须手动在类的构造函数中来初始化这个变量。而定义成纯虚函数,就是我心里的小算盘了,你继承了我的类,但是没有调用我的宏,那是编译不过去的!看我是多么的为类的使用者——和我自己——而着想啊!另外还有一个static版的_st_object_real_type,功能是一样的,目的是在没有类对象的情况下,也能够得到类名称。而且,我计划在FuncItem类中的特定地方通过 T::_st_object_real_type 得到原始类名,这样,实例化的FuncItem对象就必须是包含有_st_object_real_type调用的类了,这样的类要么是你自己定义_st_object_real_type(什么人会这么干呢),要么调用Registe_Handle_Class宏,也就是继承自HandleBase,于是,我们事件处理器的安全性又被加强了。

接下来说说check_HandleBase_derived,这看上去似乎是所有定义里最诡异的地方了。其实,我之前已经保证了所有继承自HandleBase的类都必须调用宏Registe_Handle_Class,而check_HandleBase_derived就是为了保证所有调用了宏Registe_Handle_Class的类,都必须继承自HandleBase。因为,如果你没有继承自HandleBase,那么,是找不到check_HandleBase_derived的实现的。同时,check_HandleBase_derived被直接定义在类的声明中,所以,会被看成是inline而直接被函数体所替换,因此,_object_real_type不会因为多调用了一次check_HandleBase_derived而产生更多的栈资源开销。


唉,写了这么多,可是还没有完,因为我们既然已经将类的原始名称记录了下来,那么,就和typeid的name操作说再见吧,修改一下func_call函数:

复制代码
void func_call(HandleBase* obj_ptr, void* param)
{
    
if!obj_ptr )
        
return ;

    
if( typeid(*obj_ptr)==typeid(T) )
        (((T
*)obj_ptr)->*handler)(param);
    
else
        printf(
"Runtime error:get %s object while expecting %s\n", obj_ptr->_object_real_type(), T::_st_object_real_type());
}
复制代码


怎么样?万事大吉了吗?没有!别怪没完没了,因为我实在不想把这个话题再延续到下一篇博文了,就让我们在这里一并都解决了吧!

看看您键盘上的Ctrl、C、V是不是磨损最严重的呢?呵呵,干我们这行,这几个键是基本功,不是我不鼓励创新啊,可是有些地方相似的代码直接拷贝过来,是最效率的办法,当然,我们的许多错误,也是这样产生的,就像现在一样!我已经写好了ClassA,它继承了HandleBase,我还需要一个ClassB,也一样要继承自HandleBase,不用多说,拷贝ClassA的一部分定义过来(拷贝多少就看您自己了),改个类名不就完了嘛。呵呵,在此,我估计2/3的朋友会忘记更改Registe_Handle_Class宏参数中的类名称为ClassB!不是我低估您,如果您真的想到了这一点,那您是属于那1/3部分的。虽然这个疏漏并不影响代码运行,但是,真的运行时出错的话,我们可能会被错误提示搞得一头雾水,比如:

Runtime error:get ClassA object while expecting ClassA

为了您着想,我从心眼里想帮您解决这个问题,可是,有办法吗?当然有!我们不是有宏嘛,随便在宏里添加自己的代码,反正使用的人又不知道!那么,就让我们来检查一下这个类的类型吧,修改宏定义如下:

复制代码
#define Registe_Handle_Class(x) \
public: \
    
const char* _object_real_type() { return x::check_HandleBase_derived(#x); } \
    
static const char* _st_object_real_type() { return x::check_HandleBase_derived(#x); } \
private: \
    
static void _no_use_fun_for_private() {} \
    
void _test_class_match_ok() { x::_no_use_fun_for_private(); }
复制代码


前半部分没变,变得是添加了private部分内容。好了,先不分析代码,让我们把刚才说的场景带到这里来试一下。如果在ClassB的定义中,调用了Registe_Handle_Class(ClassA),于是,private部分会被展开成这样:

private: \
    
static void _no_use_fun_for_private() {} \
    
void _test_class_match_ok() { ClassA::_no_use_fun_for_private(); }


注意,这可是在ClassB的定义中!那么,调用ClassA的私有成员函数就成了一件不可能完成的任务了!

唉,现在真的大功告成了,是不是代码有些乱了?代码有些多了,就不再帖上来,请您点击这里下载最终的代码,并和我一起来检验一下我们的成果吧!

先正常运行一下:

I'm ClassA, show_me is called! param is 0xff00
I'm ClassB, come_on is called! param is 0x0


好,将ClassA的定义中去掉 public HandleBase,保留对宏Registe_Handle_Class的调用,编译出错:

BlogTest.C:81: error: ‘check_HandleBase_derived’ is not a member of ‘ClassA’


再将 public HandleBase 恢复,注释掉对宏Registe_Handle_Class的调用,编译出错:

BlogTest.C:149: error: cannot declare variable ‘ca’ to be of abstract type ‘ClassA’
BlogTest.C:
80: note:   because the following virtual functions are pure within ‘ClassA’:
BlogTest.C:
11: note:    virtual const char* HandleBase::_object_real_type()

呵呵,这就验证了Registe_Handle_Class宏和继承HandleBase是必须同时出现的。下面恢复ClassA的正常定义,将ClassB定义中对宏Registe_Handle_Class调用的参数改为 ClassA,编译出错:


BlogTest.C: In member function ‘void ClassB::_test_class_match_ok()’:
BlogTest.C:
81: error: ‘static void ClassA::_no_use_fun_for_private()’ is private
BlogTest.C:
94: error: within this context

OK,看来想骗我也是没这么容易的了!再来注释掉ClassB中继承HandleBase的代码和调用宏Registe_Handle_Class的代码,同时注释掉main函数中向ClassB的对象cb发送事件的代码,编译出错:

BlogTest.C:155:   instantiated from here
BlogTest.C:
58: error: ‘_st_object_real_type’ is not a member of ‘ClassB’


哈哈,太帅了,不继承HandleBase就别想在事件处理器中注册!好,恢复所有的正常代码,现在该验证我们的RTTI了!修改main函数中对发送EVENT_TEST_1事件的处理对象为cb:

send_event(EVENT_TEST_1, /*&ca*/&cb, (void*)0xff00);  // modify ca to cb


编译没问题,看运行结果:

Runtime error:get ClassB object while expecting ClassA
I
'm ClassB, come_on is called! param is 0x0


哇,我们的目的达到了!看看我们的成果吧,有没有一种满足感呢?


欢迎您把自己的意见写下来,大家一起讨论,如果我的文章能帮上你的忙,我就真得很满足了!

 

转:http://www.cnblogs.com/Xiao_bird/archive/2009/07/09/1520058.html
0 0
原创粉丝点击