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

http://blog.chinaunix.net/uid-790245-id-2037519.html


书接上文。上篇的最后,提到了事件注册程序存在的风险,现在让我们来着手解决吧!


首先,我们来分析一下风险形成的原因:
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来做为类型判断的工具。不过,在实现这个想法时,还遇到了一个小波折。

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

 1 class  FuncCaller
 2 {
 3public:
 4    virtual void func_call(void* obj_ptr, void* param)=0;
 5
 6}
;
 7
 8 template  < typename T >
 9 class  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


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


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


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值