下面把 C++ 的 RTTI(Run-Time Type Information,运行时类型信息)系统一次讲清楚,覆盖原理、用法、细节与坑,并给出精简示例。内容“精准”,不绕弯。

什么是 RTTI

RTTI 是编译器在多态场景下,为对象保留其动态类型信息,以便在运行时查询和安全转换。标准接口主要有两件事:

  • typeid:拿到对象(或类型)的类型描述 std::type_info
  • dynamic_cast:做安全的向下转型(downcast)/交叉转型(cross-cast)。

触发条件:多态类(类里至少有一个 virtual 函数)。非多态类也能用 typeid(T) 或对非多态表达式typeid(expr)(这时只给静态类型)。

两个核心工具

typeid

  • 语义:

    • typeid(T):返回编译期类型 Ttype_info(无运行期开销)。

    • typeid(expr)

      • expr多态类型的左值/泛左值,结果是动态类型
      • 否则结果是静态类型,不求值 expr(无副作用)。
  • 返回 const std::type_info&,可比较相等,但 .name() 字符串是实现相关的,不要拿来做稳定键。

  • 异常:typeid(*p)pnullptr 且目标类型是多态,会抛 std::bad_typeid

示例:

1
2
3
4
5
6
7
8
9
10
11
12
#include <typeinfo>
#include <iostream>

struct B { virtual ~B() = default; };
struct D : B {};

int main() {
B* pb = new D;
std::cout << (typeid(*pb) == typeid(D)) << "\n"; // 1(动态类型)
B b;
std::cout << (typeid(b) == typeid(B)) << "\n"; // 1(静态类型)
}

要作为 map 键:用 std::type_index(对 type_info 的可哈希包装)。

1
2
3
4
5
6
#include <typeindex>
#include <unordered_map>

std::unordered_map<std::type_index, int> counts;
counts[typeid(int)]++;
counts[typeid(double)]++;

type_info 自动转 type_index 使用了 type_index 的构造函数

dynamic_cast

  • 用法:dynamic_cast<T*>(p)dynamic_cast<T&>(r)dynamic_cast<T&&>(x)

  • 目的:安全向下转型 / 交叉转型(多重继承)/ 转为 void*(取完整对象起始地址)。

  • 成功/失败:

    • 指针版失败返回 nullptr
    • 引用版失败抛 std::bad_cast
  • 对多态性的要求:

    • 向下/交叉转型:源类型必须是多态
    • 向上转型(派生→基类):允许且不要求多态(和 static_cast 类似)。
  • dynamic_cast<void*>(p)(源为多态指针)得到最派生对象的起始地址。

示例(向下/交叉转型):

1
2
3
4
5
6
7
8
9
10
11
struct Base { virtual ~Base() = default; };
struct Left : virtual Base {};
struct Right : virtual Base {};
struct Most : Left, Right {};

Base* b = new Most;
// 向下
if (auto m = dynamic_cast<Most*>(b)) { /* ok */ }
// 交叉:从 Left* 转到 Right*
Left* l = dynamic_cast<Left*>(b);
if (auto r = dynamic_cast<Right*>(l)) { /* ok */ } // 需要 RTTI 支持

运行时开销与对象布局(直观层面)

底层原理:RTTI 信息如何存储

上面讲了 typeid/dynamic_cast 的用法,但它们的“魔法”来自编译器在幕后生成的一套元数据。这一节以主流的 Itanium C++ ABI(GCC/Clang 采用)为主线,讲清楚这些信息到底存在哪、长什么样。

vptr 与 vtable

只有多态类才“享受”RTTI。编译器为每个含虚函数的类生成一张虚函数表(vtable),并为每个多态对象在布局首部(或对应子对象首部)安插一个隐藏指针 vptr,指向该类的 vtable。

%%{init: {"themeVariables": {"fontSize": "18px"}}}%%
classDiagram
  direction LR

  class Obj["多态对象实例"] {
    +VTable* vptr
    +object data ...
  }

  class VT["类的 vtable"] {
    +ptrdiff_t offset_to_top
    +type_info* typeinfo
    +virtual destructor
    +virtual function slots
  }

  class TI["std::type_info"] {
    +TypeInfoVTable* vptr
    +const char* name
    +base_info[] bases
  }

  Obj --> VT : vptr
  VT --> TI : typeinfo

关键点:vtable 的第一个槽位(或某个固定偏移)存的是指向该类型 std::type_info 对象的指针。这就是 RTTI 与虚函数机制复用同一套基础设施的根源——只要能拿到 vptr,就能拿到 type_info。

type_info 的存储

std::type_info 是标准库类型,但它的真实实现是编译器内部 ABI 类型(Itanium ABI 中称为 __class_type_info / __si_class_type_info / __vmi_class_type_info 等子类)。每个多态类型在程序的全局数据区(只读段)有一个唯一的 type_info 实例:

  • 它持有该类型的 mangled name(修饰名,如 7DerivedN3Foo3BarE)。std::type_info::name() 返回的就是这个修饰名;调试看到的“可读名”是 demangle 后的结果。
  • 它包含一个虚表指针,因此 type_info 自身也“多态”——这正是 Itanium ABI 用不同子类编码不同继承形态的方式。
  • typeid 的相等比较(operator==)通常不是逐字节比字符串,而是先比 type_info 对象的地址(同一类型在进程内只有一个实例,地址相等即同类),地址不同再回退到名字比较。这就是为什么 typeid(a) == typeid(b) 很快。

继承关系的编码:三种 type_info 子类

dynamic_cast 之所以能做安全转型,靠的是 type_info 里编码的继承图。Itanium ABI 用不同子类表达不同复杂度:

type_info 子类适用场景存了什么
__class_type_info没有基类(或仅非多态)只有自己
__si_class_type_info单继承一个指向唯一公开基类 type_info 的指针
__vmi_class_type_info多重/虚继承一个数组:每个元素是 {基类 type_info 指针, 偏移标志位}

第三种最关键:多重/虚继承时,每个基类条目里存着该基类子对象相对于最派生对象的偏移,以及若干标志位(是否 public、是否 virtual 等)。这张表就是 dynamic_cast 在运行时“寻路”的地图。

dynamic_cast 的运行时算法(概念)

给定 dynamic_cast<T*>(p),运行库大致这样工作:

  1. 通过 p 的 vptr 找到源类型的 vtable,进而拿到源类型的 type_info
  2. 通过 type_info 里的 vptr 找到最派生对象(complete object)的 type_info。
  3. 在最派生对象的继承图(上面那张基类表)里搜索目标类型 T
    • 找到 → 计算出 T 子对象相对于最派生对象的偏移,加上 p 调整得到结果指针。
    • 找不到(或访问性不合法)→ 返回 nullptr(指针版)/抛 std::bad_cast(引用版)。
  4. dynamic_cast<void*>(p) 跳过搜索,直接利用“最派生对象”信息返回完整对象起始地址。

所以:

  • 单继承下,偏移往往是编译期常量,搜索退化为 O(1)。
  • 多重/虚继承下,运行库要遍历继承表、解析虚基类链,成本明显更高——这就是前面“运行时开销”那段话的具体来源。
  • 这套查找逻辑由编译器在 libstdc++/libc++ 运行库里实现(如 __dynamic_cast),不是你写的代码。

一句话总结存储模型

编译器为每个多态类生成 vtable;vtable 里挂一个指向全局唯一的 type_info 实例的指针;type_info 实例里存着类型名和(多重/虚继承时的)基类偏移表。typeid 取指针做相等比较,dynamic_cast 用偏移表在继承图里“寻路”。

标准并不规定上述任何 ABI 细节(vtable 布局、type_info 内部结构、修饰名格式都是实现定义)。不同编译器、不同 ABI(如 MSVC 的 RTTI 布局与 Itanium 完全不同)互不兼容——这正解释了后面“跨模块/DLL”那条坑。

  • 每个多态对象通常有一个隐藏的vptr(指向 vtable)。vtable 里含虚函数表项,且常带指向该类型 type_info 的指针。
  • dynamic_cast 根据 RTTI 在继承图中查找正确的子对象偏移(多重/虚继承时需要求解路径)。
  • 成本:单继承下通常很快;多重/虚继承下可能更贵,但仍是“按需发生”的查询。与手写 static_cast+未定义行为相比,dynamic_cast 付出的是安全性的代价。
  • 二进制大小:启用 RTTI 会引入 type_info 实例与相关元数据。通常可接受。

一些编译器支持关闭 RTTI(如 GCC/Clang 的 -fno-rtti、MSVC 的 /GR-)。关闭后 typeid/dynamic_cast(除极少数受限情形)将不可用或受限。

关键边界条件与常见坑

  1. 多态性判断
    只有当类本身有虚函数时才是多态;仅通过基类指针/引用使用一个对象并不会“自动多态”。

  2. typeid 是否求值表达式

    • 非多态表达式:不求值(无副作用)。
    • 多态左值:会求值以拿动态类型,typeid(*p)p==nullptr 会抛 std::bad_typeid
  3. 失败处理差异

    • dynamic_cast<T*>(p) 失败→nullptrdynamic_cast<T&>(r) 失败→std::bad_cast
  4. 交叉转型

    • 只有 dynamic_cast 能安全跨多重继承分支;static_cast 不行。
  5. 对象切片

    • 若把派生对象按值赋给基类(发生切片),再 typeid 那个基类对象本身只会是 Base(静态),因为动态类型信息丢了。应通过基类引用/指针保持多态。
  6. .name()

    • 仅供调试;展示名(mangled/demangled)实现相关,不要做持久化键。需要稳定键用 std::type_index
  7. 跨模块/DLL 转型

    • 不同编译单元/ABI 的 type_info 可能不“等同”,跨 DLL 的 dynamic_cast/typeid 需保证一致的编译器/选项/运行库(工程实践注意点)。

什么时候用,什么时候别用

  • 适用

    • 必须在运行时依据真实类型做分支(日志、诊断、序列化框架、插件系统、通用容器里的异质对象等)。
    • 需要安全的向下/交叉转型。
  • 替代方案(能不用 RTTI 时更佳):

    • 虚函数/访客模式(Visitor):把“分支”下放到多态接口,避免“按类型分支”的味道。
    • CRTP/模板静态多态:编译期决策;零运行时开销。
    • std::variant + std::visit:受控的并集类型,避免不受控的类型分支。
    • 类型擦除std::anystd::function、自定义 erasure):封装行为而非暴露真实类型。

小而全的示例

typeidstd::type_index

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <typeindex>
#include <unordered_map>

struct B { virtual ~B() = default; };
struct D : B {};

int main() {
B* p = new D;
std::cout << (typeid(*p) == typeid(D)) << "\n"; // 1:动态类型
std::unordered_map<std::type_index, int> cnt;
cnt[typeid(*p)]++;
std::cout << cnt[typeid(D)] << "\n"; // 1
}

dynamic_cast 成功/失败路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <typeinfo>

struct Base { virtual ~Base() = default; };
struct Der : Base {};
struct Other: Base {};

int main() {
Base* b = new Der;

if (auto d = dynamic_cast<Der*>(b)) std::cout << "downcast ok\n";
if (auto o = dynamic_cast<Other*>(b)) std::cout << "cross cast ok\n";
else std::cout << "cross cast fail\n";

try {
Base& ref = *b;
(void)dynamic_cast<Other&>(ref); // 抛 std::bad_cast
} catch (const std::bad_cast&){ std::cout << "bad_cast\n"; }
}

dynamic_cast<void*> 获取完整对象基址

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

struct B { virtual ~B() = default; };
struct L : virtual B {};
struct R : virtual B {};
struct M : L, R {};

int main() {
M m;
R* r = &m;
void* p = dynamic_cast<void*>(r); // 指向 m 的完整对象起始地址
std::cout << (p == static_cast<void*>(&m)) << "\n"; // 通常打印 1
}

总结

  • 只在确有需要的地方启用 RTTI 分支;其它地方优先虚函数、CRTP、variant
  • 需要 downcast/cross-cast 时,用 dynamic_cast;指针判空即可安全处理失败。
  • 若必须“按类型分支”,用 std::type_index 做键,而不是 .name() 字符串。
  • 保留多态:传参优先用基类引用/指针,避免对象切片。
  • 跨库/插件设计,统一编译器与开关(RTTI/异常/运行库/ABI)以保证 typeid/dynamic_cast 可互通。
  • 性能敏感路径里,衡量 dynamic_cast 频次;能转为虚调度或 variant 访问更好。