C++中的运行中动态类型识别RTTI

RTTI综述

      C++中的2个运算符支持RTTI,即Run Time Type Identification:typeid和dynamic_cast。

      RTTI实现的基石是每个类型对应的一个const type_info类型对象,它存储了这个对象的确切类型信息。注意,一个类型对应一个type_info对象,而不是一个对象。无论是基本类型还是用户自定义类型,都需要额外的内存来存放此类型对应的type_info对象。一般情况,一个类型对应一个type_info对象。有的时候,需要为一种类型产生多个type_info对象:一个类继承自多个继承分支,并且多于或等于2个继承分支上存在多态类。

      type_info类重载了operator=()、operator!=(),另外一个常用的成员函数是name(),name成员函数返回字符串表示的类型信息。

typeid运算符

      typeid()运算符和sizeof运算符一样,是C++语言直接支持的。它以一个对象或者类型名作为参数,返回一个对应于该类型的const type_info对象(其实是这个类型对应的type_info对象的引用),表明该对象的确切类型。也可以使用typeid来查看非多态型对象和基本数据类型对象的类型信息。只不过,此时它不会去检索对象的vptr和vtable,它们根本就没有这些设施。此时typeid通过编译器维护的信息来返回结果,其结果仍然是操作数静态类型对应的type_info对象。

      在多态类的对象中,存在一个虚函数表指针vptr,指向类型对应的虚函数表vtable。vtable的第一项为一个type_info指针,指向该类型对应的type_info对象。vtable从第二项开始,就是该类型的虚函数指针了。

      对多态类对象应用typeid操作符,需要检索vptr指针,从vtable的第一项获得该对象类型对应的type_info对象。这个过程和虚函数的动态绑定是相同的,它们的代价相同。

      使用typeid的时候,需要注意:typeid()括号中的可以是引用,但使用指针的时候要解引用指针,如typeid(*p)。*p和p的type_info信息是完全不同的。另外,对空指针进行typeid调用,会抛出std::bad_typeid异常。

   1: typedef unsigned int UINT;
   2: void f()
   3: {
   4:     cout << typeid(UINT).name() << endl;    //"unsigned int"
   5:     cout << typeid(string).name() << endl;    //"string"
   6: }
   7:  
   8: class HomeElectricDevice
   9: {
  10: public:
  11:     virtual void Open() = 0;
  12:     virtual void Close() = 0;
  13:     virtual void Adjust(bool updown) = 0;
  14:     //Other virtual method
  15: private:
  16:     //common attibutes
  17: };
  18:  
  19: class ElectricFan:public HomeElectricDevice    //电扇
  20: {
  21: public:
  22:     //redefine the virtual functions inherit from its base class
  23: private:
  24:     //its own attributes
  25: };
  26:  
  27: class Television:public HomeElectricDevice    //TV
  28: {
  29: public:
  30:     //redefine the virtual functions inherit from its base class
  31:     virtual void PlayVCD();
  32: private:
  33:     //its own attributes
  34: };
  35:  
  36: enum Command{OPEN, ClOSE, ADJUST, PLAY_VCD, /*others commands*/};
  37: class DeviceControllor
  38: {
  39: public:
  40:     Command GetCommand(){...};
  41:     void ControlThem(HomeElectricDevice&);
  42: };
  43:  
  44: void DeviceControllor::ControlThem(HomeElectricDevice& device)
  45: {
  46:     Command cmd = GetCommand();
  47:     switch(cmd){
  48:         case OPEN:
  49:             device.Open();
  50:             break;
  51:         case ClOSE:
  52:             device.Close();
  53:             break;
  54:         case ADJUST:
  55:             device.Adjust(true);
  56:             break;
  57:         case PLAY_VCD:        //不可以像OPEN那样,直接通过device调用非基类的虚函数了
  58:                             //必须使用typeid了
  59:             if (typeid(device) == typeid(Television)){    //只能识别Television对象,不能识别其子类
  60:                 Television *pTemp = static_cast<Television*>(&device);
  61:                 pTemp->PlayVCD();
  62:             }else{
  63:                 cout << "This device cannot play VCD!" << endl;
  64:             }
  65:     }
  66: }

      考虑一下代码中case PLAY_VCD的情况,如果以后再加入一个家庭影院FamilyCinema:: public Television会如何呢?那么当传入一个FamilyCinema的引用的时候,将提示This device cannot play VCD!。显然不正确,我们如何扩展代码来解决这个问题呢?一个方法是加入一个if else分支,依旧用typeid来判断。但是这个方法毕竟修改了代码。有没有更好的办法呢?

      有,这就是dynamic_cast。

Dynamic_cast运算符

      可以看出,typeid不具备可扩展性,因为它返回一个对象的确切类型信息,不能将派生类匹配其父类。一个派生类如果是public继承,那么在语义上也应该可以看做基类对象。显然typeid不具备这个能力。dynamic_cast同时具有运行时类型识别和转换匹配2个功能。语法是:dynamic_cast<dest>(src);dest和src都必须为指针或者引用。其运行结果可以这样描述:如果运行时src和dest所引用的对象,是相同类型,或者存在is-a关系(public继承),则转换成功。否则失败。

      dynamic_cast可以用来转换指针和引用,不能转化对象。它只能用来转换多台类型的对象指针或引用。

      如果目标类型是指针时,成功则返回目标类型的指针,失败返回NULL。如果目标类型是引用,成功则返回引用,失败抛出std::bad_cast异常,因为不存在NULL引用。

      dynamic_cast的运行时类型识别功能的实现,和typeid一样,通过指针或引用所绑定对象的虚函数表指针vptr,从vtable的第一项的type_info指针获得对象类型对应的type_info对象。那么,dynamic_cast的第二个功能“运行时类型的转换匹配”如何实现的呢?为了实现这个功能,RTTI机制必须维护一棵继承树,dynamic_cast通过遍历这个继承树来确定一个待转换的对象和目标类型之间是否存在is-a关系。dynamic_cast的运行开销显然要比虚函数调用和typeid大,而且其开销会随着源对象类型与目标类型之间的层次的增大而增大。

      使用dynamic_cast注意:转换引用时,要有try、catch语句。转换指针时,检查是否转换结果为NULL。

   1: case PLAY_VCD:
   2:     try{                                                    //以后再添加Television的子类,也不需要修改代码
   3:         Television& tv = dynamic_cast<Television&>(device); //只要device引用的是Television或其子类FamilyCinema即可
   4:         tv.PlayVCD();
   5:     }catch(std::bad_cast&){
   6:         cout << "This device cannot play VCD!" << endl;
   7:     }
   8:     break;

总结

      RTTI和虚函数动态绑定一样,带来好处的同时,也不是免费的午餐。它们带来的执行速度和程序体积上的开销。

posted on 2010-04-27 19:38  ︶ㄣ第二名  阅读(4782)  评论(0编辑  收藏  举报