11、RTTI与typeid:运行时类型识别

说到多态,很多人第一反应就是虚函数表。但有个问题我一直觉得挺有意思——你拿到一个基类指针,怎么知道它到底指向什么类型?

嗯,这就是RTTI(Runtime Type Identification)要干的事。说白了,就是让程序在运行的时候,能认出对象的真实身份。

RTTI的原理:藏在虚表里的“身份证”

RTTI不是凭空变出来的。它依赖虚函数表。每个有虚函数的类,编译器都会在虚表里塞一个额外的指针——指向一个type_info对象。

我刚开始学的时候,以为RTTI是个很玄乎的东西。后来看了反汇编才明白,其实就是查表。你调用typeid,编译器就帮你从虚表里把这个指针取出来。

核心要点:只有多态类型(有虚函数的类)才能用RTTI。普通类用typeid,结果是在编译期就确定的。

举个例子:

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

class Derived : public Base {};

Base* p = new Derived;
// 这里typeid会去查虚表,拿到Derived的type_info
const std::type_info& info = typeid(*p);

你看,typeid(*p)返回的是Derived的类型信息。如果去掉virtual,那返回的就是Base了——因为编译器在编译期就决定了。

typeid运算符:怎么用?

typeid用起来很简单。它返回一个std::type_info的引用。你可以用它做两件事:

  • 比较两个类型是否相同
  • 获取类型的名字(不过这个名字是编译器决定的,不一定好看)
#include <typeinfo>
#include <iostream>

Base* b1 = new Base;
Base* b2 = new Derived;

if (typeid(*b1) == typeid(*b2)) {
    std::cout << "same type" << std::endl;
} else {
    std::cout << "different types" << std::endl;
    // 输出:different types
}

// 看看名字
std::cout << typeid(*b2).name() << std::endl;
// 输出可能是 "7Derived"(取决于编译器)

个人经验:我在做序列化框架时,经常用typeid来区分对象类型。但要注意,.name()返回的字符串格式是编译器相关的,别拿它做跨平台比较。

dynamic_cast:安全的向下转型

dynamic_cast是RTTI最常用的场景。你想把基类指针转成派生类指针,但又怕转错了——这时候就用dynamic_cast

它的原理其实不复杂:

  1. 先通过虚表拿到对象的真实类型
  2. 检查目标类型是不是真实类型的基类
  3. 如果是,就返回正确的指针;否则返回nullptr
Base* p = new Derived;

// 安全的向下转型
Derived* d = dynamic_cast<Derived*>(p);
if (d) {
    // 转型成功,可以安全使用d
    d->derivedMethod();
} else {
    // 转型失败
    std::cout << "不是Derived类型" << std::endl;
}

// 如果转成不相关的类型
class Other {};
Other* o = dynamic_cast<Other*>(p);  // 返回nullptr

我曾经踩过的坑:有一次我忘了检查dynamic_cast的返回值,直接用了转型后的指针。结果程序在特定路径下崩溃了——因为那个对象根本不是我想的类型。从那以后,我养成了习惯:每次dynamic_cast之后,一定检查返回值。

type_info结构:里面有什么?

std::type_info是个很小的类。它主要提供这几个接口:

接口 说明
name() 返回类型名称(编译器相关)
operator== 比较两个类型是否相同
operator!= 比较两个类型是否不同
before() 用于排序(实现<语义)
hash_code() C++11引入,返回哈希值

你想想看,这个结构其实很轻量。它不存储对象的任何数据,只存一个标识类型的内部指针。所以用typeid的开销很小——就是一次虚表查找。

// type_info的典型用法
const std::type_info& t1 = typeid(int);
const std::type_info& t2 = typeid(double);

if (t1 == t2) {
    // 不会执行
}

// 用于map的key
std::map<const std::type_info*, std::string> typeNames;
typeNames[&typeid(int)] = "int";
typeNames[&typeid(double)] = "double";

我个人的习惯:在调试阶段,我会用typeid(*obj).name()来打印对象类型。这比猜指针指向什么类型靠谱多了。但正式代码里,我尽量少用RTTI——它破坏了封装,而且有些项目会禁用RTTI来减小二进制体积。

RTTI的性能开销

很多人担心RTTI慢。其实没那么夸张。每次typeiddynamic_cast的开销,大概就是:

  • 一次虚表指针解引用
  • 一次类型信息查找
  • (对于dynamic_cast)一次类型继承链的遍历

说白了,比虚函数调用稍微重一点,但远没到需要担心的程度。我做过测试,在百万次调用级别,RTTI的开销也就几毫秒。

不过要注意:dynamic_cast在多重继承下会慢一些,因为要遍历多条继承链。但大多数场景下,你根本感觉不到。

什么时候该用RTTI?

说实话,RTTI是个双刃剑。用得好,能解决很多棘手问题;用不好,代码会变得难以维护。

我个人觉得,这几个场景适合用:

  • 序列化/反序列化框架
  • 插件系统(需要动态识别对象类型)
  • 调试工具和日志系统
  • 某些设计模式的实现(比如Visitor模式)

但如果你发现自己频繁用dynamic_casttypeid,那可能说明设计有问题。更好的做法是用虚函数来替代——让对象自己处理,而不是你去猜它的类型。

记住:RTTI是工具,不是银弹。能用多态解决的问题,就别用RTTI。

本章小结

RTTI的原理其实不复杂——就是利用虚表里藏的type_info指针。你学会了吗?

  • typeid返回type_info引用,用于比较和获取类型名
  • dynamic_cast做安全的向下转型,失败返回nullptr
  • type_info结构很轻量,主要提供name()和比较操作
  • 性能开销可接受,但别滥用

嗯,RTTI就这些。下一章我们聊聊虚函数表的内部布局——那个更有意思。


公众号:蓝海资料掘金营,微信deep3321