下面把 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):返回编译期类型 Ttype_info(无运行期开销)。

    • typeid(expr)

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

  • 异常:typeid(*p)pnullptr 且目标类型是多态,会抛 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(除极少数受限情形)将不可用或受限。

关键边界条件与常见坑

  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):封装行为而非暴露真实类型。

小而全的示例

A. typeidstd::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 访问更好。