RTTI
学习定位:现代 C++ 高阶知识。RTTI 不是普通业务代码的首选设计手段,但了解它有助于阅读框架代码、调试多态对象和判断 dynamic_cast 的代价。
RTTI 允许程序在运行时获取对象的实际类型信息,主要依赖:
- 虚函数表(vtable) 中的类型信息指针
- 需要类至少有一个虚函数(通常是虚析构函数)
1. typeid 运算符
头文件
#include <typeinfo>基本用法
const std::type_info& ti = typeid(表达式); // 表达式可以是对象、指针、类型名
const char* name = ti.name(); // 返回编译器相关的类型名对指针与多态的区别
Base* p = new Derived;
typeid(p); // 返回 type_info 对应 Base*(静态类型)
typeid(*p); // 若 Base 有虚函数,返回 Derived 的类型信息;否则返回 Base注意:typeid(*p) 时如果 p == nullptr,会抛出 std::bad_typeid 异常。
比较类型
if (typeid(*p) == typeid(Derived)) { ... }2. std::type_info 类
| 成员函数 / 操作 | 作用 |
|---|---|
name() | 返回类型名的「修饰后」字符串 |
before() | 类型排序(并非继承关系) |
operator== / != | 比较两个类型是否相同 |
hash_code() | 返回类型的哈希值(C++11) |
不能拷贝构造或赋值(拷贝构造为 delete),只能通过引用使用。
3. dynamic_cast
语法(指针)
Derived* pd = dynamic_cast<Derived*>(pb);- 成功返回
Derived*,失败返回nullptr。
语法(引用)
Derived& rd = dynamic_cast<Derived&>(rb);- 失败抛出
std::bad_cast异常。
支持的类型转换
- 向下转换(基类 → 派生类)
- 向上转换(派生类 → 基类,等价于隐式转换)
- 交叉转换(同一继承体系内的兄弟类转换)
交叉转换示例
struct A { virtual ~A() = default; };
struct B : A {};
struct C : A {};
B b;
A* pa = &b;
C* pc = dynamic_cast<C*>(pa); // 返回 nullptr,因为 pa 实际指向 B对 void* 的限制
void* pv = dynamic_cast<void*>(pb); // 合法,返回指向「最派生对象」的指针只能将 dynamic_cast 用于多态类型到 void*,且 pb 必须是多态指针。
不能用于非多态类型
struct NonPoly { };
NonPoly* np = new NonPoly;
// dynamic_cast<...>(np); // 编译错误!非多态类型4. RTTI 的实现与开销
典型实现方式
- 每个包含虚函数的类在 vtable 之前(或内部)存储一个
type_info*指针。 typeid和dynamic_cast运行时通过 vtable 查找该指针。
带来的额外开销
| 开销类型 | 具体表现 |
|---|---|
| 内存开销 | 每个多态类增加一个 type_info*(通常 8 字节,布局依赖编译器) |
| 时间开销 | - typeid:间接寻址,基本可忽略- dynamic_cast:沿继承链比较类型名称(字符串比较),开销随继承深度增加 |
| 代码体积 | 编译器需要为每个多态类生成类型信息结构(.rodata 段) |
| 优化阻碍 | 开启 RTTI 可能阻止某些虚函数优化(如 devirtualization) |
简单测试示例(感性理解性能差距)
// dynamic_cast 与静态转换的性能对比(假设确定类型时)
Base* b = new Derived;
for (int i = 0; i < 1e8; ++i) {
Derived* d = dynamic_cast<Derived*>(b); // 需类型检查
}相比 static_cast(假设开发者已经确定类型安全),dynamic_cast 可能慢数倍到数十倍(取决于继承深度和编译器实现)。
如何禁用 RTTI
- GCC/Clang:
-fno-rtti - MSVC:
/GR-
禁用后:
typeid对所有类型返回静态类型信息(对多态对象亦如此)dynamic_cast对多态类型编译错误- 可减少二进制体积(尤其嵌入式环境)
5. 何时使用 RTTI?何时避免?
可使用的场景
- 实现
operator==判断两个基类指针是否指向同一派生类型 - 安全向下转型(但优先考虑虚函数 + 访问者模式)
- 插件系统 / 复杂框架的序列化
应避免的场景
- 性能关键路径(如每帧调用数千次
dynamic_cast) - 可以用虚函数替代的地方(优先使用虚函数)
- 低内存 / 嵌入式环境(禁用 RTTI)
- 继承层次很深且频繁转型(字符串比较代价高)
6. 补充:decltype(不属于 RTTI)
decltype 是编译时类型推导,与运行时无关,无任何运行时开销。
int x = 10;
decltype(x) y = 20; // y 的类型为 int
const int& z = x;
decltype(z) w = y; // w 为 const int&常用场景:
- 模板中依赖表达式的类型
- 完美转发(
decltype(std::forward<T>(arg)))
7. 总结对比表
| 特性 | typeid | dynamic_cast | decltype |
|---|---|---|---|
| 发生时机 | 运行时 | 运行时 | 编译时 |
| 主要用途 | 获取类型信息 / 比较类型 | 安全转换指针/引用 | 推导表达式类型 |
| 前置要求 | 多态类型需虚函数,否则静态类型 | 基类必须有虚函数 | 无 |
| 开销 | 极低(查 vtable) | 中等(字符串比较) | 零(编译期) |
| 异常 | 对空指针解引用抛出异常 | 引用失败抛 std::bad_cast | 无 |
最后提醒:
RTTI 是一把双刃剑。它增加了代码的易用性,但会带来运行时开销和内存占用。在 C++ 中,优先使用虚函数和多态设计,只在必要时使用 RTTI。如果开发嵌入式或实时系统,考虑使用 -fno-rtti 彻底关闭它。