RTTI

学习定位:现代 C++ 高阶知识。RTTI 不是普通业务代码的首选设计手段,但了解它有助于阅读框架代码、调试多态对象和判断 dynamic_cast 的代价。

RTTI 允许程序在运行时获取对象的实际类型信息,主要依赖:

  • 虚函数表(vtable) 中的类型信息指针
  • 需要类至少有一个虚函数(通常是虚析构函数)

1. typeid 运算符

头文件

#include <typeinfo>

基本用法

const std::type_info& ti = typeid(表达式);   // 表达式可以是对象、指针、类型名
const char* name = ti.name();                // 返回编译器相关的类型名

对指针与多态的区别

Base* p = new Derived;
typeid(p);      // 返回 type_info 对应 Base*(静态类型)
typeid(*p);     // 若 Base 有虚函数,返回 Derived 的类型信息;否则返回 Base

注意typeid(*p) 时如果 p == nullptr,会抛出 std::bad_typeid 异常。

比较类型

if (typeid(*p) == typeid(Derived)) { ... }

2. std::type_info

成员函数 / 操作作用
name()返回类型名的「修饰后」字符串
before()类型排序(并非继承关系)
operator== / !=比较两个类型是否相同
hash_code()返回类型的哈希值(C++11)

不能拷贝构造或赋值(拷贝构造为 delete),只能通过引用使用。


3. dynamic_cast

语法(指针)

Derived* pd = dynamic_cast<Derived*>(pb);
  • 成功返回 Derived*,失败返回 nullptr

语法(引用)

Derived& rd = dynamic_cast<Derived&>(rb);
  • 失败抛出 std::bad_cast 异常。

支持的类型转换

  1. 向下转换(基类 → 派生类)
  2. 向上转换(派生类 → 基类,等价于隐式转换)
  3. 交叉转换(同一继承体系内的兄弟类转换)

交叉转换示例

struct A { virtual ~A() = default; };
struct B : A {};
struct C : A {};
 
B b;
A* pa = &b;
C* pc = dynamic_cast<C*>(pa);   // 返回 nullptr,因为 pa 实际指向 B

void* 的限制

void* pv = dynamic_cast<void*>(pb);  // 合法,返回指向「最派生对象」的指针

只能将 dynamic_cast 用于多态类型void*,且 pb 必须是多态指针。

不能用于非多态类型

struct NonPoly { };
NonPoly* np = new NonPoly;
// dynamic_cast<...>(np);   // 编译错误!非多态类型

4. RTTI 的实现与开销

典型实现方式

  • 每个包含虚函数的类在 vtable 之前(或内部)存储一个 type_info* 指针。
  • typeiddynamic_cast 运行时通过 vtable 查找该指针。

带来的额外开销

开销类型具体表现
内存开销每个多态类增加一个 type_info*(通常 8 字节,布局依赖编译器)
时间开销- typeid:间接寻址,基本可忽略
- dynamic_cast:沿继承链比较类型名称(字符串比较),开销随继承深度增加
代码体积编译器需要为每个多态类生成类型信息结构(.rodata 段)
优化阻碍开启 RTTI 可能阻止某些虚函数优化(如 devirtualization)

简单测试示例(感性理解性能差距)

// dynamic_cast 与静态转换的性能对比(假设确定类型时)
Base* b = new Derived;
for (int i = 0; i < 1e8; ++i) {
    Derived* d = dynamic_cast<Derived*>(b);  // 需类型检查
}

相比 static_cast(假设开发者已经确定类型安全),dynamic_cast 可能慢数倍到数十倍(取决于继承深度和编译器实现)。

如何禁用 RTTI

  • GCC/Clang: -fno-rtti
  • MSVC: /GR-

禁用后:

  • typeid 对所有类型返回静态类型信息(对多态对象亦如此)
  • dynamic_cast 对多态类型编译错误
  • 可减少二进制体积(尤其嵌入式环境)

5. 何时使用 RTTI?何时避免?

可使用的场景

  • 实现 operator== 判断两个基类指针是否指向同一派生类型
  • 安全向下转型(但优先考虑虚函数 + 访问者模式)
  • 插件系统 / 复杂框架的序列化

应避免的场景

  • 性能关键路径(如每帧调用数千次 dynamic_cast
  • 可以用虚函数替代的地方(优先使用虚函数
  • 低内存 / 嵌入式环境(禁用 RTTI)
  • 继承层次很深且频繁转型(字符串比较代价高)

6. 补充:decltype(不属于 RTTI)

decltype编译时类型推导,与运行时无关,无任何运行时开销。

int x = 10;
decltype(x) y = 20;       // y 的类型为 int
const int& z = x;
decltype(z) w = y;        // w 为 const int&

常用场景:

  • 模板中依赖表达式的类型
  • 完美转发(decltype(std::forward<T>(arg))

7. 总结对比表

特性typeiddynamic_castdecltype
发生时机运行时运行时编译时
主要用途获取类型信息 / 比较类型安全转换指针/引用推导表达式类型
前置要求多态类型需虚函数,否则静态类型基类必须有虚函数
开销极低(查 vtable)中等(字符串比较)零(编译期)
异常对空指针解引用抛出异常引用失败抛 std::bad_cast

最后提醒
RTTI 是一把双刃剑。它增加了代码的易用性,但会带来运行时开销内存占用。在 C++ 中,优先使用虚函数和多态设计,只在必要时使用 RTTI。如果开发嵌入式或实时系统,考虑使用 -fno-rtti 彻底关闭它。