C++ RTTI

 

RTTI是"Runtime Type Information"的缩写,意思是运行时类型信息,它提供了运行时确定对象类型的方法。

1 RTTI 简介

  • RTTI(Runtime Type Identification)是“运行时类型识别”的意思。C++引入这个机制是为了让程序在运行时能根据基类的指针或引用来获得该指针或引用所指的对象的实际类型。但是现在RTTI的类型识别已经不限于此了,它还能通过typeid操作符识别出所有的基本类型的变量对应的类型。

  • 为什么会出现RTTI这一机制呢?这和C++语言本身有关系,C++是一门静态类型语言,其数据类型是在编译期就确定的,不能在运行时更改。然而由于面向对象程序设计中多态性的要求,C++中的指针或引用本身的类型,可能与它实际代表的类型并不一致,有时我们需要将一个多态指针转换为其实际指向对象的类型,就需要知道运行时的类型信息,这就有了运行时类型识别需求。和Java相比,C++要想获得运行时类型信息,只能通过RTTI机制,并且C++最终生成的代码是直接与机器相关的。

  • 在C++中,RTTI包含以下三个元素:

    • type_info:该结构存储了有关特定类型的信息

    • typeid:该运算符返回其表达式或类型名的实际类型

    • dynamic_cast:该运算符将基类的指针或引用安全地转换为派生类类型的指针或引用(也就是所谓的下行转换)

2 type_info 详解

  • typeid的返回值是const type_info&类型的数据,下面是type_infogcc-4.9.0中的定义(位于libstdc++-v3\libsupc++\typeinfo文件中

      class type_info
      {
      public:
          virtual ~type_info();
    
          const char* name() const _GLIBCXX_NOEXCEPT
          { return __name[0] == '*' ? __name + 1 : __name; }
    
      #if !__GXX_TYPEINFO_EQUALITY_INLINE
          bool before(const type_info& __arg) const _GLIBCXX_NOEXCEPT;
          bool operator==(const type_info& __arg) const _GLIBCXX_NOEXCEPT;
      #else
          #if !__GXX_MERGED_TYPEINFO_NAMES
              bool before(const type_info& __arg) const _GLIBCXX_NOEXCEPT
              { return (__name[0] == '*' && __arg.__name[0] == '*') ?
                  __name < __arg.__name : __builtin_strcmp (__name, __arg.__name) < 0;
              }
    
              bool operator==(const type_info& __arg) const _GLIBCXX_NOEXCEPT
              {
                  return ((__name == __arg.__name) ||
                      (__name[0] != '*' && __builtin_strcmp (__name, __arg.__name) == 0));
              }
          #else
              bool before(const type_info& __arg) const _GLIBCXX_NOEXCEPT
              { return __name < __arg.__name; }
    
              bool operator==(const type_info& __arg) const _GLIBCXX_NOEXCEPT
              { return __name == __arg.__name; }
          #endif
      #endif
    
          bool operator!=(const type_info& __arg) const _GLIBCXX_NOEXCEPT
          { return !operator==(__arg); }
    
      #if __cplusplus >= 201103L
          size_t hash_code() const noexcept
          {
          #  if !__GXX_MERGED_TYPEINFO_NAMES
              return _Hash_bytes(name(), __builtin_strlen(name()),
                  static_cast<size_t>(0xc70f6907UL));
          #  else
              return reinterpret_cast<size_t>(__name);
          #  endif
          }
      #endif // C++11
    
          virtual bool __is_pointer_p() const;
    
          virtual bool __is_function_p() const;
    
          virtual bool __do_catch(const type_info *__thr_type, void **__thr_obj,
              unsigned __outer) const;
    
          virtual bool __do_upcast(const __cxxabiv1::__class_type_info *__target,
              void **__obj_ptr) const;
    
      protected:
          const char *__name;
    
          explicit type_info(const char *__n): __name(__n) { }
    
      private:
          type_info& operator=(const type_info&);
          type_info(const type_info&);
      };
    
    

    从源代码中可以看出以下几点内容:

    • 有一个类成员__name,类型是const char*,这个指针最终会指向类型的名字

    • 我们不能直接实例化类type_info的对象,因为该类的正常构造函数是保护的,要构造type_info对象的唯一方法就是使用typeid运算符。

    • 由于重载的赋值运算符和拷贝构造函数也是私有的,因此我们不能自己去复制或分配类type_info的对象。

    • 其余那些成员方法大家就自己看一下吧,都是一些从名字就能看出用法的函数,比如name()返回类型名,__is_pointer_p()返回是否是指针类型等等

3 type_id 详解

  • type_id 识别静态类型

    当typeid中的操作数是以下任意一种时,typeid得出的是静态类型,即编译时就确定的类型:

    1. 一个任意的类型名

    2. 一个基本内置类型的变量,或指向基本内置类型的指针或引用

    3. 一个任意类型的指针(指针就是指针,本身不体现多态,多指针解引用才有可能会体现多态)

    4. 一个具体的对象实例,无论对应的类有没有多态都可以直接在编译器确定

    5. 一个指向没有多态的类对象的指针的解引用

    6. 一个指向没有多态的类对象的引用

    由于静态类型在程序的运行过程中并不会改变,所以并不需要等到程序运行时再去推算其类型,在编译时期就能根据操作数的静态类型,从而推导出其具体类型信息。我们先来看如下一段代码:

      #include <iostream>
      #include <string>
      #include <vector>
      #include <typeinfo>
    
      int main(int argc, char* argv[])
      {
          std::cout << typeid(char).name() << std::endl;
          std::cout << typeid(int).name() << std::endl;
          std::cout << typeid(double).name() << std::endl;
    
          std::cout << "----------" << std::endl;
    
          std::cout << typeid(unsigned char).name() << std::endl;
          std::cout << typeid(unsigned int).name() << std::endl;
    
          std::cout << "----------" << std::endl;
    
          std::cout << typeid(std::string).name() << std::endl;
          std::cout << typeid(std::vector<float>).name() << std::endl;
      }
    

    运行结果如下(gcc-4.8.5):

      c
      i
      d
      ----------
      h
      j
      ----------
      Ss
      St6vectorIfSaIfEE
    

    由上可以看出,type_id获取的是被编译器转换后的类型名。我们可以通过以下代码获取正常的名字:

      #include <iostream>
      #include <string>
      #include <vector>
      #include <typeinfo>
      #include <cxxabi.h>
    
      const char* TypeToName(const char* name)
      {
          const char* __name = abi::__cxa_demangle(name, nullptr, nullptr, nullptr);
          return __name;
      }
    
      int main(int argc, char* argv[])
      {
          std::cout << TypeToName(typeid(char).name()) << std::endl;
          std::cout << TypeToName(typeid(int).name()) << std::endl;
          std::cout << TypeToName(typeid(double).name()) << std::endl;
    
          std::cout << "----------" << std::endl;
    
          std::cout << TypeToName(typeid(unsigned char).name()) << std::endl;
          std::cout << TypeToName(typeid(unsigned int).name()) << std::endl;
    
          std::cout << "----------" << std::endl;
    
          std::cout << TypeToName(typeid(std::string).name()) << std::endl;
          std::cout << TypeToName(typeid(std::vector<float>).name()) << std::endl;
      }
    

    这里用到了abi::__cxa_demangle这个方法获取类型的真实名称,结果如下:

      char
      int
      double
      ----------
      unsigned char
      unsigned int
      ----------
      std::string
      std::vector<float, std::allocator >
    

    完整测试代码如下:

      #include <iostream>
      #include <string>
      #include <vector>
      #include <typeinfo>
      #include <cxxabi.h>
    
      const char* TypeToName(const char* name)
      {
          const char* __name = abi::__cxa_demangle(name, nullptr, nullptr, nullptr);
          return __name;
      }
    
      class A
      {
      public:
          void print()
          {
              std::cout << "A" << std::endl;
          }
      };
    
      class B : public A
      {
      public:
          void print()
          {
              std::cout << "B" << std::endl;
          }
      };
    
      class C
      {
      public:
          virtual void print()
          {
              std::cout << "C" << std::endl;
          }
      };
    
      class D : public C
      {
      public:
          virtual void print()
          {
              std::cout << "D" << std::endl;
          }
      };
    
      int main(int argc, char* argv[])
      {
          std::cout << "---------- 一个任意的类型名 ----------" << std::endl;
          std::cout << "----- 基本内置类型名 -----" << std::endl;
          std::cout << TypeToName(typeid(char).name()) << std::endl;
          std::cout << TypeToName(typeid(int).name()) << std::endl;
          std::cout << "----- 无多态的类型名 -----" << std::endl;
          std::cout << TypeToName(typeid(std::string).name()) << std::endl;
          std::cout << TypeToName(typeid(std::vector<int>).name()) << std::endl;
          std::cout << "----- 有多态的类型名 -----" << std::endl;
          std::cout << TypeToName(typeid(std::iostream).name()) << std::endl;
    
          std::cout << "\n\n" << "---------- 一个基本内置类型的变量,或指向基本内置类型的指针或引用 ----------" << std::endl;
          std::cout << "----- 基本内置类型的变量 -----" << std::endl;
          long type_long = 1;
          double type_double = 1.23;
          std::cout << TypeToName(typeid(type_long).name()) << std::endl;
          std::cout << TypeToName(typeid(type_double).name()) << std::endl;
          std::cout << "----- 指向基本内置类型的指针或引用 -----" << std::endl;
          long* type_long_ptr = &type_long;
          double& type_double_ref = type_double;
          std::cout << TypeToName(typeid(type_long_ptr).name()) << std::endl;
          std::cout << TypeToName(typeid(type_double_ref).name()) << std::endl;
    
          std::cout << "\n\n" << "---------- 一个任意类型的指针 ----------" << std::endl;
          std::cout << "----- 一个指向没有多态类型的指针 -----" << std::endl;
          B b;
          A* a_ptr = &b;
          std::cout << TypeToName(typeid(&b).name()) << std::endl;
          std::cout << TypeToName(typeid(a_ptr).name()) << std::endl;
          std::cout << "----- 一个指向具有多态类型的指针 -----" << std::endl;
          D d;
          C* c_ptr = &d;
          std::cout << TypeToName(typeid(&d).name()) << std::endl;
          std::cout << TypeToName(typeid(c_ptr).name()) << std::endl;
    
          std::cout << "\n\n" << "---------- 一个具体的对象实例,无论对应的类有没有多态都可以直接在编译器确定 ----------" << std::endl;
          std::cout << "----- 无多态类的实例 -----" << std::endl;
          std::string type_string;
          std::vector<int> type_vector;
          std::cout << TypeToName(typeid(type_string).name()) << std::endl;
          std::cout << TypeToName(typeid(type_vector).name()) << std::endl;
          std::cout << "----- 有多态类的实例 -----" << std::endl;
          std::iostream type_ios(nullptr);
          std::cout << TypeToName(typeid(type_ios).name()) << std::endl;
    
          std::cout << "\n\n" << "---------- 一个指向没有多态的类对象的指针的解引用 ----------" << std::endl;
          std::cout << "----- 一个指向没有多态的类对象的指针的解引用 -----" << std::endl;
          std::cout << TypeToName(typeid(*a_ptr).name()) << std::endl; // 推算出来的依然是A
    
          std::cout << "\n\n" << "---------- 一个指向没有多态的类对象的引用 ----------" << std::endl;
          std::cout << "----- 一个指向没有多态的类对象的引用 -----" << std::endl;
          A& a_ref = b;
          std::cout << TypeToName(typeid(a_ref).name()) << std::endl; // 推算出来的依然是A
      }
    

    结果如下:

      ---------- 一个任意的类型名 ----------
      ----- 基本内置类型名 -----
      char
      int
      ----- 无多态的类型名 -----
      std::string
      std::vector<int, std::allocator >
      ----- 有多态的类型名 -----
      std::iostream
    
    
      ---------- 一个基本内置类型的变量,或指向基本内置类型的指针或引用 ----------
      ----- 基本内置类型的变量 -----
      long
      double
      ----- 指向基本内置类型的指针或引用 -----
      long*
      double
    
    
      ---------- 一个任意类型的指针 ----------
      ----- 一个指向没有多态类型的指针 -----
      B*
      A*
      ----- 一个指向具有多态类型的指针 -----
      D*
      C*
    
    
      ---------- 一个具体的对象实例,无论对应的类有没有多态都可以直接在编译器确定 ----------
      ----- 无多态类的实例 -----
      std::string
      std::vector<int, std::allocator >
      ----- 有多态类的实例 -----
      std::iostream
    
    
      ---------- 一个指向没有多态的类对象的指针的解引用 ----------
      ----- 一个指向没有多态的类对象的指针的解引用 -----
      A
    
    
      ---------- 一个指向没有多态的类对象的引用 ----------
      ----- 一个指向没有多态的类对象的引用 -----
      A
    
  • typeid 识别动态类型

    当typeid中的操作数是以下任意一种时,typeid需要在程序运行时推算类型,因为其操作数的类型在编译时期是不能被确定的:

    • 一个指向含有多态的类对象的指针的解引用

    • 一个指向含有多态的类对象的引用

    下面先来看一个典型错误案例,程序如下:

      #include <iostream>
      #include <string>
      #include <vector>
      #include <typeinfo>
      #include <cxxabi.h>
    
      const char* TypeToName(const char* name)
      {
          const char* __name = abi::__cxa_demangle(name, nullptr, nullptr, nullptr);
          return __name;
      }
    
      class A
      {
      public:
          void print()
          {
              std::cout << "A" << std::endl;
          }
    
          int a;
      };
    
      class B : virtual public A
      {
      public:
          void print()
          {
              std::cout << "B" << std::endl;
          }
    
          int b;
      };
    
      class C : virtual public A
      {
      public:
          void print()
          {
              std::cout << "C" << std::endl;
          }
    
          int c;
      };
    
      class D : public B, public C
      {
      public:
          void print()
          {
              std::cout << "D" << std::endl;
          }
    
          int d;
      };
    
      int main(int argc, char* argv[])
      {
          D d;
          A* a_ptr = &d;
          B* b_ptr = &d;
          C* c_ptr = &d;
    
          std::cout << TypeToName(typeid(d).name()) << std::endl;
          std::cout << TypeToName(typeid(*a_ptr).name()) << std::endl;
          std::cout << TypeToName(typeid(*b_ptr).name()) << std::endl;
          std::cout << TypeToName(typeid(*c_ptr).name()) << std::endl;
      }
    

    运行结果如下;

      D
      A
      B
      C
    

    从运行结果可以看出上面的代码并没有体现出多态性。这是因为单纯虚继承是无法体现出多态的,尽管它也会生成对应的虚表,要真正实现多态只能依靠虚函数来实现。

    修改类A如下,其余不变:

      class A
      {
      public:
          virtual ~A() {}
    
          void print()
          {
              std::cout << "A" << std::endl;
          }
    
          int a;
      };
    

    结果如下:

      D
      D
      D
      D
    

    可以看到给类A加了个虚析构函数之后,多态性就体现出来了,typeid也可以正确识别出a_ptrb_ptrc_ptr引用的真实类型了。

  • typeid实现原理

    虚函数表指针指向的前一个位置(也就是索引的-1项)存放的就是当前实际类型的type_info信息, 上例的实例 D d 的内存布局如下图所示:

    图中:

    • vbase_offset代表当前虚表指针距离虚基类的地址偏移,如果没有虚继承,则没有此项。

    • offset_to_top代表虚表指针距离此类起始地址的偏移。

    • typeinfo for D指向了类D的运行时信息,可以通过此结构体获取继承信息。

    因此我们可以利用typeinfo获取信息,在上一个例子的基础上,进行如下修改:

      int main(int argc, char* argv[])
      {
          D d;
          A* a_ptr = &d;
          B* b_ptr = &d;
          C* c_ptr = &d;
    
          std::cout << typeid(*a_ptr).name() << std::endl;
          long* vtbl_A = (long*)*(long*)a_ptr;
          std::cout << ((std::type_info*)*(vtbl_A - 1))->name() << std::endl << std::endl;
    
          std::cout << typeid(*b_ptr).name() << std::endl;
          long* vtbl_B = (long*)*(long*)b_ptr;
          std::cout << ((std::type_info*)*(vtbl_B - 1))->name() << std::endl << std::endl;
    
          std::cout << typeid(*c_ptr).name() << std::endl;
          long* vtbl_C = (long*)*(long*)c_ptr;
          std::cout << ((std::type_info*)*(vtbl_C - 1))->name() << std::endl;
      }
    

    运行结果如下:

      1D
      1D
    
      1D
      1D
    
      1D
      1D
    

    从运行结果可以看到,我们在程序中将虚函数表的-1项的值转换成一个type_info的指针类型,并调用name()成员函数的最终的输出为1D,与typeid的输出一致,从而可以知道,typeid关于多态类型的计算是通过基类指针或引用指向的对象(子对象)的虚函数表的-1项获得的。

    补充说明:在多继承或虚继承的情况下,派生类类有n(n>1)个虚函数指针,分别指向其各个基类的虚函数表的具体位置(事实上只有一个表,这些表是链接在一起的),但是一个类的所有虚函数表的索引为-1项的值(type_info对象的地址)都是相等的,即它们都指向同一个type_info对象,这样就实现了无论使用了哪一个基类的指针或引用指向其派生类的对象,都能通过相应的虚函数表获取到相同的type_info对象,从而得到相同的类型信息。

3 dynamic_cast 详解

  • dynamic_cast 详情见 C++ 类型转换。

  • 这里讨论一下以下两个问题

  1. RTTI 的信息如何和对象绑定在一起?什么时候绑定的?

    虚表上面的元素都属一个叫做Derive_Class::RTTI Complete Object Locator的结构体 , 这个结构指向该类的名字,和其对象继承链。

    这就回答了第一个问题,RTTI info 如何和对象绑定的? 在对象创建的时候,调用构造函时候,创建虚表以及RTTI info,这样dynamic cast 就可以去访问RTTI,从而保证安全。

  2. 为什么dynam_cast 必须要求转换的类型之间要有虚函数?否则编译通不过。

    RTTI info 存在于虚表的第一项。第二个问题就可以回答,因为RTTI 依赖于虚表,所以用dynamic_cast 对应的类一定要有虚函数。