云计算百科
云计算领域专业知识百科平台

C++ RTTI 与 dynamic_cast 底层实现深度解析

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 性能影响因素排名

  • 虚继承:最大的性能杀手(5-10x 开销),需要运行时查询虚基类偏移表
  • 继承深度:每增加一层继承约增加 2-3 ns(GCC 单继承链遍历)
  • 基类数量:多继承下基类数组遍历,每个基类条目增加少量开销
  • 缓存局部性:虚表、RTTI 结构和 type_info 字符串的缓存命中率

  • 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(取决于继承复杂度)
    • 收益:提供安全的运行时类型转换,是许多设计模式(访问者、工厂、序列化框架)的基础设施

    使用准则

  • 首选设计避免 dynamic_cast:多态虚函数 > visitor 模式 > dynamic_cast
  • 不得不使用时:优先用引用版本的 dynamic_cast<T&>,失败时抛出 std::bad_cast 便于定位问题
  • 性能敏感路径:使用 static_cast + 手动标记替代 dynamic_cast(如枚举类型 ID)
  • 跨模块边界:注意 RTTI 的 type_info 可见性问题,必要时使用 -Wl,-Bsymbolic 或 RTLD_GLOBAL
  • 嵌入式场景:考虑 -fno-rtti 并自建类型标识系统
  • 赞(0)
    未经允许不得转载:网硕互联帮助中心 » C++ RTTI 与 dynamic_cast 底层实现深度解析
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!