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-rttidynamic_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/Linux7MyClassItanium ABI mangled name(数字是名称长度)
Clang/Linux7MyClass与 GCC 兼容
Clang/Mac7MyClass 或 MyClass取决于版本
MSVCclass 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)MSVCO(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 ns1.0x(基线)
多继承 Base0* → DerivedMI*(同偏移基类)~5 ns1.25x
多继承 Base0* → DerivedMI*(跨基类,需调整指针)~8 ns2.0x
虚继承 Base0* → DerivedVI*~25 ns6.25x
无效转换(返回 nullptr)~3 ns0.75x

9.3 性能影响因素排名

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

10. 编译器差异对比

10.1 核心差异汇总

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

使用准则

  1. 首选设计避免 dynamic_cast:多态虚函数 > visitor 模式 > dynamic_cast
  2. 不得不使用时:优先用引用版本的 dynamic_cast<T&>,失败时抛出 std::bad_cast 便于定位问题
  3. 性能敏感路径:使用 static_cast + 手动标记替代 dynamic_cast(如枚举类型 ID)
  4. 跨模块边界:注意 RTTI 的 type_info 可见性问题,必要时使用 -Wl,-Bsymbolic 或 RTLD_GLOBAL
  5. 嵌入式场景:考虑 -fno-rtti 并自建类型标识系统
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

翎_鸢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值