书接上文。上篇的最后,提到了事件注册程序存在的风险,现在让我们来着手解决吧!
首先,我们来分析一下风险形成的原因:
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
哇,我们的目的达到了!看看我们的成果吧,有没有一种满足感呢?
欢迎您把自己的意见写下来,大家一起讨论,如果我的文章能帮上你的忙,我就真得很满足了!