文章详情

  • 游戏榜单
  • 软件榜单
关闭导航
热搜榜
热门下载
热门标签
php爱好者> php文档>C++模板实现事件处理器中的“通用成员函数指针”..

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

时间:2010-03-27  来源:zhm_sunboy

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


首先,我们来分析一下风险形成的原因:
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

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


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

排行榜 更多 +
辰域智控app

辰域智控app

系统工具 下载
网医联盟app

网医联盟app

运动健身 下载
汇丰汇选App

汇丰汇选App

金融理财 下载