c++ RTTI
下面把 C++ 的 RTTI(Run-Time Type Information,运行时类型信息)系统一次讲清楚,覆盖原理、用法、细节与坑,并给出精简示例。内容“精准”,不绕弯。
什么是 RTTI
RTTI 是编译器在多态场景下,为对象保留其动态类型信息,以便在运行时查询和安全转换。标准接口主要有两件事:
typeid
:拿到对象(或类型)的类型描述std::type_info
。dynamic_cast
:做安全的向下转型(downcast)/交叉转型(cross-cast)。
触发条件:多态类(类里至少有一个
virtual
函数)。非多态类也能用typeid(T)
或对非多态表达式用typeid(expr)
(这时只给静态类型)。
两个核心工具
1. typeid
语义:
typeid(T)
:返回编译期类型T
的type_info
(无运行期开销)。typeid(expr)
:- 若
expr
是多态类型的左值/泛左值,结果是动态类型。 - 否则结果是静态类型,不求值
expr
(无副作用)。
- 若
返回
const std::type_info&
,可比较相等,但.name()
字符串是实现相关的,不要拿来做稳定键。异常:
typeid(*p)
若p
为nullptr
且目标类型是多态,会抛std::bad_typeid
。
示例:
#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
的可哈希包装)。
#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 的构造函数
2. 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)
(源为多态指针)得到最派生对象的起始地址。
示例(向下/交叉转型):
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 支持
运行时开销与对象布局(直观层面)
- 每个多态对象通常有一个隐藏的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):封装行为而非暴露真实类型。
小而全的示例
A. typeid
与 std::type_index
#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
}
B. dynamic_cast
成功/失败路径
#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"; }
}
C. dynamic_cast<void*>
获取完整对象基址
#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
}
实战建议(Checklist)
- 只在确有需要的地方启用 RTTI 分支;其它地方优先虚函数、CRTP、
variant
。 - 需要 downcast/cross-cast 时,用
dynamic_cast
;指针判空即可安全处理失败。 - 若必须“按类型分支”,用
std::type_index
做键,而不是.name()
字符串。 - 保留多态:传参优先用基类引用/指针,避免对象切片。
- 跨库/插件设计,统一编译器与开关(RTTI/异常/运行库/ABI)以保证
typeid
/dynamic_cast
可互通。 - 性能敏感路径里,衡量
dynamic_cast
频次;能转为虚调度或variant
访问更好。