运行时类型信息RTTI

时间:2021-12-10 03:06:56

我们在写C++代码的时候经常碰到使用dynamic_cast进行类型转换的情况,也都知道经过dynamic_cast的转换更加安全,因为dynamic_cast进行了类型检查。 但是可能很多人不知道dynamic_cast是C++ 运行时类型信息(RTTI)机制链条上的一个节点。 RTTI提供了两个操作符和一个类:

  • dynamic_cast
  • typeid
  • type_info

整个RTTI, 作为一个整体,暴露给程序员的就是这三个元素。因此我们关注的焦点也就在它们身上了。


什么是RTTI

在C++中存在虚函数,也就存在了多态性,对于多态性的对象,其基类的指针可以指向任何派生类的对象,这时就有可能不知道基类指针到底指向的是哪个对象的情况,类型的确定要在运行时利用运行时类型标识做出。RTTI 就是语言内建的支持运行时识别对象的类型并提供类型信息的一套机制。


typeid 和 type_info

  • typeid 是一个操作符,提供对象的类型信息,基本用法是:
#include <typeinfo>
#include <iostream>

class Person {
   public: 
   virtual ~Person() {}
};

class Employee : public Person { };

int main()
{
    Person person;
    Employee employee;
    Person* ptr = &employee;
    Person& ref = employee;

    std::cout << typeid(int).name() << std::endl;
    std::cout << typeid(Employee).name() << std::endl; 
    std::cout << typeid(*ptr).name() << std::endl; 
    std::cout << typeid(ptr).name() << std::endl;
    std::cout << typeid(ref).name() << std::endl;

    return 0;
}

输出的结果是

int
class Employee
class Employee
class Person*
class Employee

从上面的代码可以看出,typeid操作符最常见的用法就是在运行时提供对象的真实类型信息(name), 接受的可以是 一个类型名或者一个表达式。
当输入的是一个指针时,typeid会识别为指针类型(class Person*而不是class Person), 但是typeid并不会把引用识别为引用,因此不会有class Person&这样的结果。也就是说输入引用和输入对象对于typeid是一回事,但是对于指针你得解引用!

  • 注意这里的类型或对象可以不是多态类型。
class Account{}; //没有虚函数非多态类型
Account acc;
Account& ref = acc;
std::cout << typeid(ref).name() << std::endl; //ok, 输出 class Account

虽然typeid支持这种非多态类型的查询,但是这些信息实际上在编译时就已经知道了,因此这样的用法更多得是为了完整性考虑,而没有多大的实际用处。

  • 抛出异常
Person* ptr = NULL;
void*   vptr = ptr;

try{
   std::cout << typeid(vptr).name() << std::endl;
}
catch(std::bad_typeid& e)
{ std::cout << "bad_typeid caught on vptr" << std::endl; }
catch(...){}

try{

   std::cout << typeid(ptr).name() << std::endl;
}
catch(std::bad_typeid& e)
{ std::cout << "bad_typeid caught on ptr" << std::endl; }
catch(...){}

try{ 
  std::cout << typeid(*ptr).name() << std::endl;
}
catch(std::bad_typeid& e)
{ std::cout << "bad_typeid caught on *ptr" << std::endl; }
catch(...){}

输出的结果是

void*
Person*
bad_typeid caught on *ptr

上面的例子可以看到对于空指针,typeid不会深入检查指针是否为空,而是仅仅返回指针的类型,但是解引用空指针则会引发bad_typeid异常!

  • typeid 返回type_info引用
const std::type_info& tinforef = typeid( int ); //编译通过
const std::type_info tinfo = typeid( int );     //编译错误

type_info类型具有私有的拷贝构造函数和赋值函数,因此typeid返回的引用不能被赋给type_info对象,同样type_info也没有公有构造函数因此不能被创建,只能通过这种引用的方式获取它的实例!

  • type_info 声明
class type_info {
public:
    virtual ~type_info();
    bool operator==(const type_info& rhs) const noexcept;
    bool operator!=(const type_info& rhs) const noexcept;
    bool before(const type_info& rhs) const noexcept;
    size_t hash_code() const noexcept;
    const char* name() const noexcept;
private:
    type_info(const type_info& rhs);
    type_info& operator=(const type_info& rhs);
};

这里的”==”判断类型是否一样,而不是判定两个指针指向同一个对象。

const std::type_info& tinforef = typeid( Employee ); 
Employee employee;
Person* ptr = &employee;
if( tinforef == typeid( *ptr ) ) //判断成立,都是Employee类型
{...}

dynamic_cast

作为 RTTI 三大件之一, dynamic_cast可能是最重要的了!前面我们已经讲过了dynamic_cast的用法,而且也指出dynamic_cast必须是作用于多态类型,下面的代码会编译错误, 因为Person没有虚函数不是多态类型!

class Person {};
class Employee : public Person {};
Employee employee;
Person* ptr = &employee;
Employee* emp = dynamic_cast<Employee*>(ptr);

RTTI 实现模型

关于RTTI到底怎么实现,标准没有指出,因此不同的编译器有不同的实现方法。这里为了便于理解RTTI的运行原理给出了一个实现模型!
这里编译器在vtable里面添加了一项type_info*指针,指向type_info结构,每个类型有一个type_info数据段。 从这里可以看出编译器提供关闭RTTI选项的作用了,由于编译器需要为每个多态类型产生一个type_info结构,关闭RTTI确实可以省出不少空间!
运行时类型信息RTTI


RTTI 的消耗

RTTI的消耗到底是多少,具体的情况肯定取决于编译器的实现和实际的类型,比如类型名的长度等等。


编译器选项

Visual Studio 和 GCC 默认都打开RTTI支持,但是你也可以关闭该选项,在Visual Studio下,Properties->Configuration Properties->C/C++->Language,右面Enable Run-Time Type Information选择No(/GR-). GCC使用-fno-rtti关闭RTTI支持。

此时使用dynamic_cast,typeid都会抛出std::__non_rtti_object异常!显然这是因为这时候编译器并没有为多态类型生成type_info数据,而dynamic_cast,typeid都会用到这一信息!

需要注意的是关闭了RTTI 并不一定导致运行时异常,只有在切实需要type_info信息而RTTI又关闭的时候才会抛出异常。 比如上行转化的时候即使使用了dynamic_cast且关闭了RTTI仍然不会有问题!