C++ RTTI
下面把 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):返回编译期类型T的type_info(无运行期开销)。typeid(expr):- 若
expr是多态类型的左值/泛左值,结果是动态类型。 - 否则结果是静态类型,不求值
expr(无副作用)。
- 若
返回
const std::type_info&,可比较相等,但.name()字符串是实现相关的,不要拿来做稳定键。异常:
typeid(*p)若p为nullptr且目标类型是多态,会抛std::bad_typeid。
示例:
1 | |
要作为 map 键:用
std::type_index(对type_info的可哈希包装)。
1 | |
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 | |
运行时开销与对象布局(直观层面)
底层原理: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(修饰名,如
7Derived、N3Foo3BarE)。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),运行库大致这样工作:
- 通过
p的 vptr 找到源类型的 vtable,进而拿到源类型的type_info。 - 通过
type_info里的 vptr 找到最派生对象(complete object)的 type_info。 - 在最派生对象的继承图(上面那张基类表)里搜索目标类型
T:- 找到 → 计算出
T子对象相对于最派生对象的偏移,加上p调整得到结果指针。 - 找不到(或访问性不合法)→ 返回
nullptr(指针版)/抛std::bad_cast(引用版)。
- 找到 → 计算出
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(除极少数受限情形)将不可用或受限。
关键边界条件与常见坑
多态性判断
只有当类本身有虚函数时才是多态;仅通过基类指针/引用使用一个对象并不会“自动多态”。typeid是否求值表达式- 非多态表达式:不求值(无副作用)。
- 多态左值:会求值以拿动态类型,
typeid(*p)且p==nullptr会抛std::bad_typeid。
失败处理差异
dynamic_cast<T*>(p)失败→nullptr;dynamic_cast<T&>(r)失败→std::bad_cast。
交叉转型
- 只有
dynamic_cast能安全跨多重继承分支;static_cast不行。
- 只有
对象切片
- 若把派生对象按值赋给基类(发生切片),再
typeid那个基类对象本身只会是Base(静态),因为动态类型信息丢了。应通过基类引用/指针保持多态。
- 若把派生对象按值赋给基类(发生切片),再
.name()- 仅供调试;展示名(mangled/demangled)实现相关,不要做持久化键。需要稳定键用
std::type_index。
- 仅供调试;展示名(mangled/demangled)实现相关,不要做持久化键。需要稳定键用
跨模块/DLL 转型
- 不同编译单元/ABI 的
type_info可能不“等同”,跨 DLL 的dynamic_cast/typeid需保证一致的编译器/选项/运行库(工程实践注意点)。
- 不同编译单元/ABI 的
什么时候用,什么时候别用
适用:
- 必须在运行时依据真实类型做分支(日志、诊断、序列化框架、插件系统、通用容器里的异质对象等)。
- 需要安全的向下/交叉转型。
替代方案(能不用 RTTI 时更佳):
- 虚函数/访客模式(Visitor):把“分支”下放到多态接口,避免“按类型分支”的味道。
- CRTP/模板静态多态:编译期决策;零运行时开销。
std::variant+std::visit:受控的并集类型,避免不受控的类型分支。- 类型擦除(
std::any、std::function、自定义 erasure):封装行为而非暴露真实类型。
小而全的示例
typeid 与 std::type_index
1 | |
dynamic_cast 成功/失败路径
1 | |
dynamic_cast<void*> 获取完整对象基址
1 | |
总结
- 只在确有需要的地方启用 RTTI 分支;其它地方优先虚函数、CRTP、
variant。 - 需要 downcast/cross-cast 时,用
dynamic_cast;指针判空即可安全处理失败。 - 若必须“按类型分支”,用
std::type_index做键,而不是.name()字符串。 - 保留多态:传参优先用基类引用/指针,避免对象切片。
- 跨库/插件设计,统一编译器与开关(RTTI/异常/运行库/ABI)以保证
typeid/dynamic_cast可互通。 - 性能敏感路径里,衡量
dynamic_cast频次;能转为虚调度或variant访问更好。





