第20.8章:RTTI 与 dynamic_cast

English

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. 用什么替代它

注意每一种替代方案按构造都是独立的,因为它们都不需要一个知道你类名的运行时。