第20.8章:RTTI 与 dynamic_cast
1. 这个特性
运行期类型信息是让 typeid、dynamic_cast 和处理块匹配能够工作的东西。P1105R0 第 4.3 节 提议在独立模式里把它做成可选的,理由你大概猜得到:每个类一个表、每个类型名一个字符串,外加一个必须遍历类层级的运行时函数。
单个类的 RTTI 是一个 type_info 对象;对有基类的类,那是一个 __si_class_type_info,它自己有一张 vtable,而整个结构由 C++ ABI 描述,而不是由标准描述。所以代价不是"几十字节的元数据":而是每个类多一张 vtable,外加一个知道如何比较和遍历这些对象的运行时。
2. 实测:一次 dynamic_cast 要求什么
struct B { virtual ~B() noexcept = default; };
struct D : B { int v{}; };
int probe(B &b) noexcept {
if (dynamic_cast<D *>(&b) != nullptr) { return 1; }
return 0;
}
$ g++ -std=c++26 -fno-exceptions -O2 -c rtti.cc && nm -u rtti.o
U _ZTVN10__cxxabiv117__class_type_infoE
U _ZTVN10__cxxabiv120__si_class_type_infoE
U __dynamic_cast
$ g++ -std=c++26 -fno-exceptions -O2 -nostdlib rtti.cc -o rtti
undefined reference to `__dynamic_cast
undefined reference to `vtable for __cxxabiv1::__class_type_info
undefined reference to `vtable for __cxxabiv1::__si_class_type_info
九行代码三个符号,全部来自 C++ 运行时库(libsupc++ 或 libc++abi),没有一个是独立的。而且注意这是在已经加了 -fno-exceptions 的情况下:RTTI 是一项独立的依赖,不是异常的后果。这就是为什么 19.7 的禁用清单里还有 -fno-rtti。
$ g++ -std=c++26 -fno-rtti -O2 -c rtti.cc
rtti.cc:5:7: error: 'dynamic_cast' not permitted with '-fno-rtti'
3. 为什么这个代价比看上去更糟
| RTTI 的开销 | 落在哪里 |
|---|---|
每个多态类一个 type_info 对象 |
只读数据,类名作为字符串存在二进制里 |
| 每个派生类多一张 vtable | 数据加重定位,程序里每个类一份 |
| 一个 ABI 定义的运行时入口点 | __dynamic_cast:无法可移植地写出来,用了就不能省 |
| 遗留异常的处理块匹配 | 每个 catch 子句都在运行期比较类型信息 |
编译器对你点名的每个类型提供 typeid 支持 |
即使完全没有 cast,一句 typeid 表达式也会把那个对象拖进来 |
在桌面上这些都不重要。在一块 64 KiB flash、你的类层级有 20 KiB 预算的板子上,光类型名字符串就可能是个真实的数字。这就是为什么独立构建把它关掉,也是为什么"就用 dynamic_cast 呗"不能作为给面向 MCU 的人的建议。
4. 更大的论证,与体积无关
本书已经论证过(18.5)dynamic_cast 即使在能用的地方也是错误的工具:它比 typeid 贵得多,它的失败模式是一个没人检查的空指针,而避开它的代码改用未检查的 static_cast,那正是类型混淆的来源。解法不是更多 RTTI,而是在静态就已知类型 —— 封闭层级、标签、variant,或者反射。
独立实现把这个论证变得更锋利,因为它把诱惑整个拿掉了。如果 RTTI 不可用,你就写不出依赖它的设计,而会写出类型在调用点已知的那种设计。那不是妥协;那是本来就会更快更清晰的设计。
5. 用什么替代它
-
带你自己控制的判别值的封闭层级 —— 对象里一个枚举,或者一个标签类型(15.3)。代价是一个整数,到处都能用,而且
switch会被编译器做穷尽性检查。 - 带已知函数表的类型擦除(18.6)—— 操作就在表里,根本不需要 cast。
- 静态多态:模板和概念(第15章)在编译期解析类型,而那本来就是类型已知的地方。
-
反射,如果它将来存在,是
dynamic_cast那个问题的真正答案:它按名字给你成员,不需要遍历层级。在那之前,标签就是工具。
注意每一种替代方案按构造都是独立的,因为它们都不需要一个知道你类名的运行时。