1. RTTI 概念与启用条件
1.1 什么是 RTTI
RTTI(Run-Time Type Information / Runtime Type Identification)是 C++ 提供的一套运行时类型查询机制,允许程序在运行阶段获取对象的实际类型信息。RTTI 是 typeid 运算符和 dynamic_cast 运算符的底层支撑。
从编译器实现角度看,RTTI 并非一个独立的子系统,而是由三部分协同构成:
| 组成部分 | 作用 | 存储位置 |
| std::type_info 对象 | 存储类型的哈希/名称等静态信息 | 数据段(.rodata) |
| 虚表中的 RTTI 指针 | 连接多态对象与其 type_info | 虚表条目 |
| 运行时转换算法 | 遍历类层次结构完成转换 | 编译器生成的辅助函数 |
不了解虚表的可以先学习一下前面的内容:C++ 虚函数表深度剖析:从单继承到虚拟继承的内存布局-CSDN博客
三者关系如下:
多态对象 → vptr → vtable[-1] → type_info*
↘ type_info 对象的 name() / hash_code() / before()
1.2 启用条件
RTTI 不是对所有类型都可用,需要满足以下条件之一:
// 条件一:类至少有一个虚函数(即多态类型)
class Base {
public:
virtual ~Base() = default; // 虚析构函数即可
};
// 条件二:虚继承(即使没有虚函数,虚基类也会产生 vptr)
class VirtualBase {};
class Derived : virtual public VirtualBase {};
// 条件三:编译器扩展(如 MSVC 的 /GR 默认开启)
// 非多态类型的 typeid 返回静态类型,不涉及运行时查询
关键限制:对于非多态类型,typeid 返回的是编译期静态类型,dynamic_cast 只能用于多态类型的向下转型(downcast)。
struct NonPoly {};
NonPoly np;
const auto& ti = typeid(np); // OK,但返回的是静态类型 NonPoly
// dynamic_cast<NonPoly*>(&np); // 编译错误:NonPoly 不是多态类型
1.3 编译器开关
| 编译器 | 启用 RTTI | 禁用 RTTI | 禁用的影响 |
| GCC/Clang | 默认开启 | -fno-rtti | dynamic_cast 和 typeid 对多态类型不可用;typeid 对非多态类型仍可用 |
| MSVC | 默认开启(/GR) | /GR- | 同上,且部分异常处理机制可能受影响 |
禁用 RTTI 的典型场景:嵌入式系统、游戏引擎(如 Unreal Engine 部分模块)、追求极致二进制体积的项目。禁用后通常需要自建类型识别系统(如用整型枚举 ID 替代)。
2. type_info 结构与编译器实现
2.1 标准定义
C++ 标准定义了 std::type_info 类的接口(来自 <typeinfo> 头文件):
namespace std {
class type_info {
public:
virtual ~type_info();
bool operator==(const type_info& rhs) const noexcept;
bool operator!=(const type_info& rhs) const noexcept;
bool before(const type_info& rhs) const noexcept;
size_t hash_code() const noexcept;
const char* name() const noexcept;
type_info(const type_info&) = delete;
type_info& operator=(const type_info&) = delete;
};
}
接口设计有几个值得注意的特点:
- 禁止拷贝:type_info 对象是编译器在数据段生成的单例,每个类型对应唯一实例
- before() 排序:定义了类型间的全序关系,用于在 std::map 等有序容器中做键
- hash_code():C++11 引入,返回类型的哈希值,同一类型跨翻译单元保证一致
- name() 不可移植:返回值是实现定义的,不同编译器格式完全不同
2.2 GCC / Itanium ABI 实现
GCC 和 Clang 遵循 Itanium C++ ABI。Itanium ABI 将 RTTI 数据结构设计为层次化系统:
__type_info (基类)
├── __fundamental_type_info (基本类型: int, double 等)
├── __array_type_info (数组类型)
├── __function_type_info (函数类型)
├── __enum_type_info (枚举类型)
├── __class_type_info (普通类/结构体)
├── __si_class_type_info (单继承非虚继承类)
├── __vmi_class_type_info (多继承或虚继承类)
└── __pbase_type_info (指针类型,含 const/volatile 修饰)
其核心数据结构(简化版):
// 基类:所有 RTTI 节点的基础
class __type_info {
protected:
const char* __type_name; // 经过名称修饰(name-mangled)的类型名字符串
public:
// … operator== / operator!= 等成员
};
// 单继承类:额外包含指向唯一基类的 RTTI 指针
class __si_class_type_info : public __class_type_info {
public:
const __class_type_info* __base_type; // 唯一直接基类的 RTTI
};
// 多继承/虚继承类:包含基类表
class __vmi_class_type_info : public __class_type_info {
public:
unsigned int __flags; // 标志位(如是否包含虚继承)
unsigned int __base_count; // 基类数量
__base_class_type_info __base_info[1]; // 可变长度数组,每个基类的详细信息
};
struct __base_class_type_info {
const __class_type_info* __base_type; // 基类的 RTTI 指针
long __offset_flags; // 偏移量与标志位打包存储
// __offset_flags 的低 8 位是标志:
// bit 0: __virtual_mask (是否为虚基类)
// bit 1: __hwm_bit (是否有公共基类)
// 高位: __offset_shift (基类子对象在派生类中的偏移量)
};
__vmi_class_type_info 中的 __flags 字段区分以下情况:
| flags 值 | 含义 | 使用的 dynamic_cast 算法 |
| __diamond_shaped_mask | 钻石型继承(含虚继承的菱形) | 需要访问虚基类偏移表 |
| __non_diamond_repeat_mask | 非钻石型多继承(重复基类但无虚继承) | 标准 vmi 搜索 |
| 其他 | 普通多继承 | 标准 vmi 搜索 |
2.3 MSVC 实现
MSVC 的实现更加紧凑,RTTI 以 RTTICompleteObjectLocator 为核心结构:
RTTICompleteObjectLocator
├── signature (魔数,用于验证)
├── offset (this 调整量)
├── cdOffset (构造函数位移偏移量)
├── pTypeDescriptor → TypeDescriptor
│ ├── pVFTable (指向虚表的指针)
│ ├── spare (保留)
│ └── name (经过修饰的类型名称字符串)
└── pClassDescriptor → RTTIClassHierarchyDescriptor
├── signature
├── attributes
├── numBaseClasses
└── pBaseClassArray → RTTIBaseClassDescriptor[]
├── pTypeDescriptor
├── numContainedBases
├── mdisp (成员位移)
├── pdisp (虚表位移)
├── vdisp (虚基类位移)
└── attributes
MSVC 的 RTTICompleteObjectLocator 嵌入在虚表中(通常位于 vtable[-1] 位置):
虚表布局(MSVC x64):
高地址
┌─────────────────────┐
│ RTTI Complete │ ← vtable[-1] (offset = -8)
│ Object Locator* │
├─────────────────────┤
│ virtual func 1 │ ← vtable[0]
│ virtual func 2 │ ← vtable[1]
│ … │
└─────────────────────┘
低地址
2.4 name() 的不同编译器输出
type_info::name() 的输出是编译器特定的,同一类型在不同编译器下的 name() 完全不同:
#include <iostream>
#include <typeinfo>
class MyClass { virtual ~MyClass() = default; };
int main() {
MyClass obj;
std::cout << typeid(obj).name() << std::endl;
}
| 编译器 | name() 输出 | 说明 |
| GCC/Linux | 7MyClass | Itanium ABI mangled name(数字是名称长度) |
| Clang/Linux | 7MyClass | 与 GCC 兼容 |
| Clang/Mac | 7MyClass 或 MyClass | 取决于版本 |
| MSVC | class MyClass | 人类可读但无标准保证 |
提示:若需要可移植的类型名称,可使用 boost::core::demangle() 或 GCC 的 __cxa_demangle() 函数。
3. typeid 的底层机制
3.1 多态类型的 typeid
当 typeid 的操作数是多态类型的引用或解引用后的指针时,编译器生成代码来在运行时获取实际类型:
class Animal { public: virtual ~Animal() = default; };
class Dog : public Animal {};
Dog dog;
Animal& ref = dog;
// 编译器将 typeid(ref) 转换为类似:
// const std::type_info& ti = *ref.__vptr[-1].__type_info_ptr;
// 本质:通过虚表间接访问 type_info 对象
std::cout << typeid(ref).name() << std::endl; // 输出 "Dog" 对应的名称
对应的 LLVM IR(简化版)大致如下:
; 加载 vptr
%vptr = load ptr, ptr %ref
; vtable[-1] 存储 type_info 指针(Itanium ABI)
%ti_ptr_addr = getelementptr ptr, ptr %vptr, i64 -1
%ti_ptr = load ptr, ptr %ti_ptr_addr
; 调用 type_info::name()
%name = call ptr @_ZNKSt9type_info4nameEv(ptr %ti_ptr)
3.2 非多态类型的 typeid
对于非多态类型,typeid 完全在编译期解析,不产生运行时开销:
struct Point { int x, y; }; // 非多态类型
Point p;
const auto& ti = typeid(p); // 编译期解析为 Point 的 type_info 引用
// 等价于:const auto& ti = typeid(Point);
// 不经过虚表,直接链接到数据段的 type_info 对象
# 生成的汇编(x86-64,简化):
lea rdi, [rip + typeinfo_for_Point] # 直接取地址,零运行时开销
3.3 type_info 的相等性
两个 type_info 对象相等的条件(Itanium ABI 实现):
bool __type_info::operator==(const __type_info& rhs) const {
// 策略一:先比较 __type_name 字符串指针(同一翻译单元内相同类型指向同一字符串)
if (__type_name == rhs.__type_name) return true;
// 策略二:指针不同时,比较字符串内容(跨动态库场景)
// 注:部分编译器实现仅比较指针,跨 .so 时可能误判 false
return __type_name[0] == '*' ? false : strcmp(__type_name, rhs.__type_name) == 0;
}
常见陷阱:在 Linux 上,当同一个动态库被 dlopen 多次加载(如 RTLD_LOCAL 模式)时,同一类型的 type_info 可能被复制多份,导致 typeid(A) == typeid(A) 返回 false。解决方法是确保使用 RTLD_GLOBAL 或让动态库链接时使用 -Wl,-Bsymbolic。
4. dynamic_cast 的三种实现策略
dynamic_cast 的核心挑战是:给定一个基类指针和目标类型,在运行时确定该指针实际指向的对象是否能安全地转换为目标类型,并在多继承/虚继承下正确调整指针值。
主流编译器采用以下三种策略之一:
4.1 策略对比总览
| 策略 | 代表编译器 | 时间复杂度 | 空间开销 | 核心思想 |
| 虚表遍历(Vtable Walk) | GCC/Clang(非虚继承路径) | O(N) N=继承深度 | 低 | 沿 vtable 中的 RTTI 链向上遍历,逐个比较 type_info |
| 类型树搜索(Type Tree Search) | GCC/Clang(虚继承路径) | O(B) B=基类数量 | 中 | 递归遍历 __vmi_class_type_info 中的基类数组 |
| 嵌入哈希表(Embedded Hash) | MSVC | O(1) ~ O(N) | 高 | 在虚表附近预计算完整继承链的 type_info 哈希表 |
| 字符串比较(String Comparison) | 某些嵌入式编译器 | O(1) 但常数大 | 低 | 直接用字符串比较代替 type_info 指针比较 |
4.2 策略一:虚表遍历(GCC/Clang 单继承路径)
这是最简单也最快的策略,适用于单继承链:
dynamic_cast<Dog*>(animal_ptr) 的执行流程:
1. 解引用 animal_ptr 获取 vptr
2. 读取 vtable[-1] 获取 type_info*(当前对象的实际类型)
3. 比较该 type_info 与目标类型 Dog 的 type_info
– 相等 → 转换成功,直接返回指针值(单继承无需调整 this)
– 不相等 → 通过 __si_class_type_info::__base_type 跳到父类的 type_info,重复步骤 3
4. 到达继承链顶端仍未匹配 → 返回 nullptr
算法伪代码:
void* __dynamic_cast_si(const void* obj_ptr,
const __class_type_info* obj_type,
const __class_type_info* target_type,
ptrdiff_t src2dst_offset) {
const void* vptr = *(const void**)obj_ptr;
const auto* rtti_ptr = ((const __class_type_info**)vptr)[-1];
// 沿单继承链向上遍历
for (const __si_class_type_info* cur = static_cast<const __si_class_type_info*>(rtti_ptr);
cur != nullptr;
cur = dynamic_cast<const __si_class_type_info*>(cur->__base_type)) {
if (cur == target_type) {
// 找到目标类型,调整指针
return (char*)obj_ptr + src2dst_offset;
}
// 如果当前已不是单继承类型,跳出循环走复杂路径
if (typeid(*cur) != typeid(__si_class_type_info)) break;
}
// 降级到通用搜索算法
return __dynamic_cast_generic(obj_ptr, obj_type, target_type, src2dst_offset);
}
4.3 策略二:类型树搜索(GCC/Clang 虚继承路径)
当类层次结构涉及多继承或虚继承时,RTTI 结构从 __si_class_type_info 变为 __vmi_class_type_info,需要遍历基类数组:
// 通用多继承 dynamic_cast 核心算法
void* __dynamic_cast_generic(const void* obj_ptr,
const __class_type_info* obj_type,
const __class_type_info* target_type,
ptrdiff_t src2dst) {
// 步骤 1:检查当前层级是否匹配
if (*obj_type == *target_type) {
return (char*)obj_ptr + src2dst;
}
// 步骤 2:根据 obj_type 的具体类型递归搜索
// 如果是单继承类型
if (const auto* si = dynamic_cast<const __si_class_type_info*>(obj_type)) {
return __dynamic_cast_generic(obj_ptr, si->__base_type, target_type, src2dst);
}
// 如果是多继承/虚继承类型
if (const auto* vmi = dynamic_cast<const __vmi_class_type_info*>(obj_type)) {
for (unsigned i = 0; i < vmi->__base_count; ++i) {
const auto& base = vmi->__base_info[i];
// 提取偏移量和标志位
bool is_virtual = base.__offset_flags & 1;
ptrdiff_t offset;
if (is_virtual) {
// 虚基类:偏移量需要通过虚基类表在运行时确定
offset = get_virtual_base_offset(obj_ptr, base);
} else {
// 非虚基类:偏移量编码在 __offset_flags 中
offset = base.__offset_flags >> 8;
}
// 递归搜索当前基类子树
void* result = __dynamic_cast_generic(
(char*)obj_ptr + offset,
base.__base_type,
target_type,
src2dst – offset
);
if (result) return result;
}
}
return nullptr; // 未找到目标类型
}
4.4 策略三:MSVC 的嵌入哈希表
MSVC 采用与 GCC 完全不同的策略——在 RTTIClassHierarchyDescriptor 中预计算完整的基类链,以数组形式存储,通过遍历该数组实现 O(N) 查找:
struct RTTIClassHierarchyDescriptor {
unsigned signature;
unsigned attributes; // 继承属性标志
unsigned numBaseClasses; // 基类数量(含自身)
RTTIBaseClassDescriptor** pBaseClassArray;
};
struct RTTIBaseClassDescriptor {
TypeDescriptor* pTypeDescriptor; // 指向 type_info 等价结构
unsigned numContainedBases; // 该基类包含的子基类数量
PMD where; // 指针成员位移(Pointer-to-Member Displacement)
unsigned attributes; // 属性标志
};
struct PMD {
ptrdiff_t mdisp; // 成员位移(member displacement)
ptrdiff_t pdisp; // 虚表位移(vtable displacement)
ptrdiff_t vdisp; // 虚基类位移(virtual base displacement)
};
MSVC 的 dynamic_cast 核心流程:
1. 从 obj 获取 vptr 2. 读取 vtable[-1] 获取 RTTICompleteObjectLocator* 3. 遍历 ClassHierarchyDescriptor 中的 pBaseClassArray – 对每个元素,比较 pTypeDescriptor 与目标类型的 TypeDescriptor – 匹配成功时,使用 PMD 中的 mdisp/pdisp/vdisp 计算正确的指针值 4. 遍历完仍未匹配 → nullptr
MSVC 这种"预计算数组"方式使得每次 dynamic_cast 都是线性扫描,但不需要像 GCC 那样递归深入继承树。在深层继承时 MSVC 可能更快(数组连续访问 vs 指针跳转),但多继承下时间复杂度均为 O(B)。
5. 单继承下的 dynamic_cast
5.1 基本向上转型(Upcast)
向上转型(派生类指针→基类指针)在编译期完成,不涉及 RTTI:
class Animal { public: virtual ~Animal() = default; };
class Dog : public Animal {};
Dog* dog = new Dog;
Animal* animal = dog; // 编译期隐式转换,无需 dynamic_cast
// 等价于:Animal* animal = static_cast<Animal*>(dog);
5.2 向下转型(Downcast)
Animal* animal = new Dog;
// dynamic_cast 在运行时验证 animal 是否真的指向 Dog
Dog* dog = dynamic_cast<Dog*>(animal);
if (dog) {
dog->bark();
}
单继承下的 dynamic_cast<Dog*> 实现非常直接:
内存布局(单继承):
┌──────────────┐
│ Animal vptr │ ← animal 和 dog 指向同一地址
├──────────────┤
│ Dog members │
└──────────────┘
转换过程:
1. 通过 vptr 获取实际类型的 type_info(此处为 Dog)
2. 沿 RTTI 链向上查找:Dog → Animal
3. 发现 Dog == 目标类型 Dog,匹配成功
4. 指针无需调整(基类子对象与派生类起始地址一致)
5.3 交叉转型(Cross-cast)
交叉转型指在同一继承层级中不同分支间的转换,单继承下不涉及此场景(需要多继承),但编译期不会报错:
class Base { public: virtual ~Base() = default; };
class A : public Base {};
class B : public Base {};
Base* base = new A;
B* b = dynamic_cast<B*>(base); // 返回 nullptr(A 不是 B)
6. 多继承下的指针调整
多继承是 dynamic_cast 最复杂的场景之一。派生类包含多个基类子对象,它们位于不同的内存偏移处。
6.1 内存布局示例
class Animal {
public:
virtual void eat() {}
virtual ~Animal() = default;
int age = 0;
};
class Flyable {
public:
virtual void fly() {}
virtual ~Flyable() = default;
double wingspan = 0.0;
};
class Bat : public Animal, public Flyable {
public:
void eat() override {}
void fly() override {}
bool echolocation = true;
};
典型内存布局(GCC/Clang x86-64):
Bat 对象内存布局:
偏移量 内容
+0 ┌──────────────┐
│ Animal vptr │ ──→ Bat-Animal 虚表
+8 │ age = 0 │
+16 ├──────────────┤
│ Flyable vptr│ ──→ Bat-Flyable 虚表(含 this 调整 thunk)
+24 │ wingspan=0.0│
+32 │ echolocation│
└──────────────┘
注:Animal 子对象从偏移 0 开始
Flyable 子对象从偏移 16 开始
6.2 指针调整的核心问题
关键问题:当持有 Animal* 指针但实际上指向 Bat 对象时,如何将其安全地转换为 Flyable*?
Animal* a = new Bat; // a 指向 Bat 对象的偏移 0(Animal 子对象起始)
Flyable* f = dynamic_cast<Flyable*>(a);
// 问题:a 指向地址 0x1000,但 Flyable 子对象在 0x1000 + 16 = 0x1010
// dynamic_cast 必须将返回值调整为 0x1010,而不简单返回 0x1000
6.3 编译器如何计算调整量
dynamic_cast 的指针调整通过 RTTI 数据结构中存储的偏移信息来实现:
GCC/Itanium ABI 方式:
struct __base_class_type_info {
const __class_type_info* __base_type;
long __offset_flags; // 偏移量编码:
// bits [0:7] = 标志位
// bits [8:63] = 偏移量(右移 8 位)
};
在 __vmi_class_type_info 的 __base_info 数组中,每个基类都记录了它在派生类对象中的偏移量。
当 dynamic_cast 遍历基类数组时,每次递归调用都会传递一个累积偏移量 src2dst:
// 初始调用:src2dst = 0(从 Animal* 的起始位置计算)
// 遍历到 Flyable 基类条目时,发现 offset = 16
// 递归调用时传递 src2dst = 0 – 16 = -16
// 在 Flyable 的 type_info 节点匹配时:
// 返回 (char*)obj_ptr + (-16) = obj_ptr – 16,即 Flyable 子对象的地址
MSVC 方式:
MSVC 使用 PMD(Pointer-to-Member Displacement)结构,在 RTTIBaseClassDescriptor 中预先存储了完整的调整参数:
struct PMD {
ptrdiff_t mdisp; // 成员位移:从派生类起始到该基类子对象的偏移
ptrdiff_t pdisp; // 虚表位移:虚表内调整
ptrdiff_t vdisp; // 虚基类位移:仅虚继承下非零
};
6.4 完整转换示例
#include <iostream>
#include <typeinfo>
class Animal {
public:
virtual const char* type() { return "Animal"; }
virtual ~Animal() = default;
};
class Flyable {
public:
virtual const char* ability() { return "Flyable"; }
virtual ~Flyable() = default;
};
class Bat : public Animal, public Flyable {
public:
const char* type() override { return "Bat"; }
const char* ability() override { return "Bat-Flyable"; }
};
int main() {
Bat bat;
Animal* a = &bat; // a 指向偏移 0
Flyable* f = &bat; // f 指向偏移 16(假设)
std::cout << "a = " << a << std::endl;
std::cout << "f = " << f << std::endl;
std::cout << "&bat = " << &bat << std::endl;
// 输出:a == &bat, f == &bat + offset_of_Flyable
// 交叉转型:从 Animal* 转到 Flyable*
Flyable* cross = dynamic_cast<Flyable*>(a);
std::cout << "cross = " << cross << std::endl;
// cross 应该等于 f,即指针值经过调整
// 无效转型
class Unrelated { public: virtual ~Unrelated() = default; };
Unrelated* u = dynamic_cast<Unrelated*>(a);
std::cout << "u = " << u << std::endl; // nullptr
return 0;
}
6.5 子对象偏移量实验
可以通过以下代码观察不同基类指针对应地址的差异:
#include <iostream>
class A {
virtual ~A() = default;
char data_a[8];
};
class B {
virtual ~B() = default;
char data_b[8];
};
class C : public A, public B {
char data_c[8];
};
int main() {
C c;
A* pa = &c;
B* pb = &c;
C* pc = &c;
std::cout << "C* = " << pc << std::endl;
std::cout << "A* = " << pa << " (offset = "
<< (char*)pa – (char*)pc << ")" << std::endl;
std::cout << "B* = " << pb << " (offset = "
<< (char*)pb – (char*)pc << ")" << std::endl;
// 验证 dynamic_cast 指针调整
B* pb2 = dynamic_cast<B*>(pa);
std::cout << "dynamic_cast<B*>(A*) = " << pb2 << std::endl;
std::cout << "pb == pb2: " << (pb == pb2) << std::endl;
}
典型输出(x86-64):
C* = 0x7ffe12345000
A* = 0x7ffe12345000 (offset = 0)
B* = 0x7ffe12345010 (offset = 16)
dynamic_cast<B*>(A*) = 0x7ffe12345010
pb == pb2: 1
7. 虚继承下的 dynamic_cast
虚继承引入了一个额外的间接层,使得 dynamic_cast 的实现更加复杂。
7.1 虚继承的内存布局
class Animal {
public:
virtual void eat() {}
int animal_id = 0;
};
class Mammal : virtual public Animal {
public:
virtual void nurse() {}
int fur_type = 0;
};
class Bird : virtual public Animal {
public:
virtual void lay_eggs() {}
int feather_color = 0;
};
class Platypus : public Mammal, public Bird {
public:
void eat() override {}
bool venomous = true;
};
典型内存布局(GCC x86-64,简化表示):
Platypus 对象内存布局:
+0 ┌───────────────────┐
│ Mammal vptr │ ──→ Mammal-in-Platypus 虚表
+8 │ fur_type │
+16 ├───────────────────┤
│ Bird vptr │ ──→ Bird-in-Platypus 虚表
+24 │ feather_color │
+32 │ venomous │
+40 ├───────────────────┤
│ Animal vptr │ ──→ Animal-in-Platypus 虚表(虚基类,仅一份)
+48 │ animal_id │
└───────────────────┘
关键点:
– Animal 只有一份(虚继承保证)
– Mammal 和 Bird 都通过虚基类表间接访问 Animal
– Animal 的偏移量在 Mammal 和 Bird 中不一定相同
7.2 虚基类偏移的运行时解析
虚继承下,基类子对象的偏移量不能在编译期确定(因为同一个类可能被放在不同派生层次的对象中)。偏移量存储在虚基类表(vbase offset table)中,通过虚表间接访问。
GCC 的实现方式:
Mammal 虚表布局(简化):
┌───────────────────────────┐
│ vbase_offset = 40 │ ← 从 Mammal 子对象起始到虚基类 Animal 的偏移
├───────────────────────────┤
│ vcall_offset (可选项) │
├───────────────────────────┤
│ type_info* (RTTI) │
├───────────────────────────┤
│ Mammal::nurse() │
│ Mammal 其他虚函数 │
└───────────────────────────┘
运行时获取虚基类偏移的伪代码:
ptrdiff_t get_virtual_base_offset(const void* obj_ptr,
const __base_class_type_info& base) {
// 虚基类的偏移存储在当前对象虚表中的特定位置
const void* vptr = *(const void**)obj_ptr;
// 从 RTTI 的 __offset_flags 中提取虚基类表的索引
// 虚基类偏移在虚表中的位置相对于 vptr
ptrdiff_t vbase_offset_index = extract_vbase_index(base.__offset_flags);
// 虚基类偏移存储在 vptr 之前的某个位置(实现定义)
const ptrdiff_t* vbase_offsets = ((const ptrdiff_t*)vptr) – vbase_offset_index – 1;
return *vbase_offsets;
}
7.3 虚继承下的 dynamic_cast 完整流程
class Animal { public: virtual ~Animal() = default; };
class Mammal : virtual public Animal {};
class Bird : virtual public Animal {};
class Platypus : public Mammal, public Bird {};
Platypus duckbill;
Animal* a = &duckbill; // a 指向虚基类 Animal
// 从虚基类 Animal* 向下转型为 Platypus*
Platypus* p = dynamic_cast<Platypus*>(a);
// 流程:
// 1. 从 a->vptr 获取 type_info(实际类型为 Platypus)
// 2. Platypus 的 RTTI 类型为 __vmi_class_type_info
// 3. 遍历基类数组(Mammal 和 Bird)
// – Mammal 是普通基类,通过它递归进入 Animal(虚基类)
// – 在 Animal 层级匹配时,需要调整 src2dst 累加偏移
// 4. 从 Animal 位置减去 Animal 子对象在 Platypus 中的偏移量
// 该偏移量通过 Mammal 的虚基类表在运行时确定
8. 跨层级转换与歧义检测
8.1 跨层级转换
dynamic_cast 支持跳过中间层级的转换:
class A { public: virtual ~A() = default; };
class B : public A {};
class C : public B {};
A* a = new C;
C* c = dynamic_cast<C*>(a); // 直接从 A* 转到 C*,跳过 B
// 编译器会自动沿继承链查找 C,无需每层手动转换
这在菱形继承中尤其重要:
class Top { public: virtual ~Top() = default; };
class Left : virtual public Top {};
class Right : virtual public Top {};
class Bottom : public Left, public Right {};
Bottom b;
Top* t = &b;
// 直接从 Top* 转到 Bottom*,跨越 Left 和 Right
Bottom* bt = dynamic_cast<Bottom*>(t);
// 编译器必须通过 Top 的 type_info 找到 Bottom,并计算正确的偏移量
8.2 歧义检测
在某些情况下,dynamic_cast 可能会遇到歧义(非虚继承菱形):
class Top { public: virtual ~Top() = default; };
class Left : public Top {}; // 非虚继承
class Right : public Top {}; // 非虚继承
class Bottom : public Left, public Right {};
Bottom b;
Top* t_left = static_cast<Left*>(&b); // 明确指向 Left 中的 Top
Top* t_right = static_cast<Right*>(&b); // 明确指向 Right 中的 Top
// 从 Top* 转到 Bottom* —— 可能产生歧义
Bottom* bt = dynamic_cast<Bottom*>(t_left);
// GCC 行为:成功转换(因为可以通过 RTTI 路径反推)
// 但如果 Top 有多个实例且无法唯一确定路径,可能失败
// 更明确的歧义场景:从虚基类指针同时匹配两条路径
// 当编译器无法确定唯一路径时,dynamic_cast 返回 nullptr
8.3 歧义的运行时检测
GCC 的 __dynamic_cast 实现会检测歧义:
// 简化版歧义检测逻辑
void* __dynamic_cast_verify(const void* obj_ptr,
const __class_type_info* obj_type,
const __class_type_info* target_type) {
void* result = nullptr;
int match_count = 0;
// 递归搜索所有基类路径
__dynamic_cast_search(obj_ptr, obj_type, target_type, 0, result, match_count);
// 找到多条路径 → 歧义
if (match_count > 1) {
return nullptr;
}
return result;
}
9. 性能基准测试
9.1 测试代码
#include <chrono>
#include <iostream>
#include <vector>
// 测试用类层次
class Base0 {
public:
virtual ~Base0() = default;
virtual int f0() { return 0; }
char padding0[64]; // 模拟真实对象大小
};
class Base1 {
public:
virtual ~Base1() = default;
virtual int f1() { return 1; }
char padding1[64];
};
class Base2 {
public:
virtual ~Base2() = default;
virtual int f2() { return 2; }
char padding2[64];
};
class DerivedSI : public Base0 {
public:
int val;
};
class DerivedMI : public Base0, public Base1, public Base2 {
public:
int val;
};
class VirtualBase : virtual public Base0 {};
class DerivedVI : public VirtualBase, public Base1 {
public:
int val;
};
template<typename From, typename To>
double benchmark_dynamic_cast(size_t iterations) {
From* ptr = new To;
ptr->val = 42;
auto start = std::chrono::high_resolution_clock::now();
for (size_t i = 0; i < iterations; ++i) {
To* result = dynamic_cast<To*>(ptr);
// 防止优化器移除调用
if (result) result->val = i;
}
auto end = std::chrono::high_resolution_clock::now();
delete ptr;
return std::chrono::duration<double, std::nano>(end – start).count() / iterations;
}
int main() {
constexpr size_t N = 10'000'000;
std::cout << "dynamic_cast 平均耗时 (ns/次, " << N << " 次测量):\\n";
std::cout << " 单继承 Base0* → DerivedSI*: "
<< benchmark_dynamic_cast<Base0, DerivedSI>(N) << " ns\\n";
std::cout << " 多继承 Base0* → DerivedMI*: "
<< benchmark_dynamic_cast<Base0, DerivedMI>(N) << " ns\\n";
std::cout << " 虚继承 Base0* → DerivedVI*: "
<< benchmark_dynamic_cast<Base0, DerivedVI>(N) << " ns\\n";
}
9.2 典型测试结果(GCC 13, -O2, x86-64)
| 转换场景 | 平均耗时 | 相对单继承倍数 |
| 单继承 Base0* → DerivedSI* | ~4 ns | 1.0x(基线) |
| 多继承 Base0* → DerivedMI*(同偏移基类) | ~5 ns | 1.25x |
| 多继承 Base0* → DerivedMI*(跨基类,需调整指针) | ~8 ns | 2.0x |
| 虚继承 Base0* → DerivedVI* | ~25 ns | 6.25x |
| 无效转换(返回 nullptr) | ~3 ns | 0.75x |
9.3 性能影响因素排名
10. 编译器差异对比
10.1 核心差异汇总
| 特性 | GCC (Itanium ABI) | MSVC | Clang |
| RTTI 结构 | 层次化 type_info 派生类 | 扁平化的 ClassHierarchyDescriptor | 与 GCC 兼容(Itanium ABI) |
| type_info 嵌入位置 | vtable[-1](虚表前一个槽位) | vtable[-1](RTTICompleteObjectLocator*) | 同 GCC |
| dynamic_cast 核心算法 | 递归遍历 RTTI 树 | 线性扫描基类描述符数组 | 同 GCC,部分路径有优化 |
| 单继承处理 | O(depth) 虚表链遍历 | O(N) 扫描(N=基类数量) | 同 GCC |
| 虚继承处理 | 递归搜索 vmi type_info | PMD.vdisp + 虚基类表 | 同 GCC |
| 空指针行为 | dynamic_cast<T*>(nullptr) → nullptr | 同左 | 同左 |
| 禁用 RTTI | -fno-rtti | /GR- | -fno-rtti |
| type_info::name() 格式 | 名称修饰字符串(7MyClass) | 人类可读(class MyClass) | 同 GCC |
10.2 GCC/Clang 的优化技巧
GCC 对某些常见模式做了特殊优化:
// 模式 1:目标类型是 final 类
class Derived final : public Base {};
Base* b = new Derived;
// 如果编译器能静态推断 b 指向 Derived,可能直接生成:
// Derived* d = static_cast<Derived*>(b); // 跳过 RTTI 开销
// 模式 2:已知成功路径
// 当编译器通过分析确定转换必然成功时,可能省略运行时检查
10.3 跨编译器兼容的注意事项
// ⚠️ 不要依赖 type_info::name() 的格式
// 错误做法:
if (std::string(typeid(obj).name()) == "MyClass") { … }
// 正确做法:
if (typeid(obj) == typeid(MyClass)) { … }
// ⚠️ 跨动态库传递 type_info 引用
// libA.so:
extern const std::type_info& get_ti();
// libB.so:
if (get_ti() == typeid(SomeType)) // 可能失败(RTLD_LOCAL 加载时)
11. 实战案例与陷阱
11.1 陷阱一:构造函数中的 dynamic_cast
class Base {
public:
Base() {
// 危险!在构造函数中调用 dynamic_cast
// 此时虚表指向 Base,而非派生类
auto* derived = dynamic_cast<Derived*>(this);
// derived 将为 nullptr
}
virtual ~Base() = default;
};
class Derived : public Base {
public:
Derived() : Base() {
// 此时虚表已经指向 Derived
auto* d = dynamic_cast<Derived*>(this);
// d 非空
}
};
原理:C++ 标准规定,在构造/析构期间,虚函数表按当前正在构造的类型逐步切换。在 Base 构造函数中,vptr 指向 Base 的虚表,因此 dynamic_cast<Derived*> 返回 nullptr。
11.2 陷阱二:空指针行为
Derived* d = nullptr;
Base* b = dynamic_cast<Base*>(d); // b 也是 nullptr,合法
Base* b_null = nullptr;
Derived* d_null = dynamic_cast<Derived*>(b_null); // 返回 nullptr,合法
// 对引用类型的空转换会抛出异常
Base& ref = *b_null; // UB!先触发了未定义行为
11.3 陷阱三:与 static_cast 的语义差异
class Base { public: virtual ~Base() = default; };
class Derived1 : public Base {};
class Derived2 : public Base {};
Base* b = new Derived1;
// static_cast:无运行时检查,直接转换
Derived2* s = static_cast<Derived2*>(b); // 编译通过!但 s 指向的是 Derived1 对象
// s->some_derived2_only_method(); // UB!
// dynamic_cast:运行时检查
Derived2* d = dynamic_cast<Derived2*>(b); // 返回 nullptr
// 安全
当确定转换合法时,static_cast 零开销;不确定时用 dynamic_cast 保安全。
11.4 陷阱四:reinterpret_cast 绕过保护
Base* b = new Derived1;
// 绝对不要这样做:
Derived2* bad = reinterpret_cast<Derived2*>(b);
// 绕过所有类型检查,必定导致未定义行为
// 即使指针值可能相同,类型系统已经被破坏
11.5 实践建议:非侵入式类型识别
当 RTTI 被禁用时,常见替代方案:
// 方案一:手动类型 ID
class GameObject {
public:
enum class Type { Player, Enemy, Projectile, NONE };
virtual Type getType() const { return Type::NONE; }
protected:
Type type_ = Type::NONE;
};
class Player : public GameObject {
public:
Player() { type_ = Type::Player; }
Type getType() const override { return Type::Player; }
};
// 使用时:
GameObject* obj = getObject();
if (obj->getType() == GameObject::Type::Player) {
auto* player = static_cast<Player*>(obj);
// …
}
// 方案二:CRTP + 类型标记(编译期多态)
template<typename Derived>
class GameObjectBase {
public:
static constexpr int TYPE_ID = -1;
};
class Player : public GameObjectBase<Player> {
public:
static constexpr int TYPE_ID = 1;
};
12. 总结
核心原理回顾
| 组件 | 核心机制 |
| type_info | 编译器为每个多态类型在数据段生成的单例对象,存储类型名称和哈希 |
| 虚表 RTTI 条目 | vtable[-1] 存储指向 type_info 的指针,连接运行时对象与其类型 |
| typeid(多态) | 通过 vptr → vtable[-1] → type_info* 间接获取,O(1) |
| dynamic_cast(单继承) | 沿 RTTI 链向上遍历比较 type_info,O(depth) |
| dynamic_cast(多继承) | 递归遍历基类数组,利用偏移量信息调整指针值,O(B) |
| dynamic_cast(虚继承) | 增加虚基类偏移表的运行时查询,开销最大,O(B) × vbase_cost |
设计取舍
RTTI 的设计体现了一个典型权衡:
- 空间开销:每个多态类型额外占用约 100-300 字节的 RTTI 数据(含 type_info 对象、名字字符串、基类数组等)
- 时间开销:typeid 基本为零(指针解引用),dynamic_cast 平均 4-25 ns(取决于继承复杂度)
- 收益:提供安全的运行时类型转换,是许多设计模式(访问者、工厂、序列化框架)的基础设施
网硕互联帮助中心


评论前必须登录!
注册