【C++进阶系列 (七)】C++ RTTI 深度剖析:从 typeid 到 dynamic_cast 的底层之旅
⭐️在这个怀疑的年代我们依然需要信仰。个人主页 YYYing.⭐️C编程系列专栏C编程系列系列上期内容【C进阶系列 (六)】C 内存对象模型从内存块到多态的底层之旅系列下期内容暂无目录1. 为什么需要 RTTI2. 核心数据结构std::type_info2.1 type_info 长什么样2.2 type_info 对象什么时候创建2.3 type_info 存在哪里3. vtable 里的秘密type_info* 藏在哪里3.1 Itanium ABI 的 vtable 布局3.2 为什么放在 vtable 里3.3 MSVC 的差异4. typeid 的完整剖析4.1 两种行为模式4.2 typeid 的底层实现4.3 空指针的陷阱4.4 type_info 的比较5. dynamic_cast 的完整剖析5.1 它到底在做什么5.2 向下转型Downcast5.3 横向转型Crosscast5.4 type_info 里到底存了什么5.5 失败与异常5.6 为什么 dynamic_cast 慢6. RTTI 的成本与禁用6.1 内存成本6.2 性能成本6.3 -fno-rtti 禁用7. 宏观总结8. 面试回答模板直接可用结语---⭐️封面自取⭐️---1. 为什么需要 RTTIC 的多态是静态类型系统 运行时派发的混合体。你写Base* p new Derived;时编译期只知道p的静态类型是Base*不知道它实际指向Derived。虚函数解决了“调用哪个函数”的问题但没解决“这个对象到底是什么类型”的问题。typeid和dynamic_cast就是为这个问题准备的。它们让程序在运行时能“问”一个对象“你究竟是谁”生动比喻虚函数像是物业的“标准服务流程”你按门铃调用虚函数物业按你的房号vtable派人。RTTI 则是你直接问物业“这栋楼里现在住的是谁”物业翻出登记簿type_info告诉你真实住户。2. 核心数据结构std::type_info2.1type_info长什么样std::type_info是标准库定义的一个类核心内容极其简单class type_info { public: virtual ~type_info(); const char* name() const noexcept; bool operator(const type_info) const noexcept; bool operator!(const type_info) const noexcept; bool before(const type_info) const noexcept; size_t hash_code() const noexcept; // 拷贝和赋值被禁用 type_info(const type_info) delete; type_info operator(const type_info) delete; protected: const char* __name; // 类型名称mangled };__name存的是修饰后的类型名mangled name比如Derived可能存成7Derived。name()返回的就是这个字符串。2.2type_info对象什么时候创建编译期。编译器在编译时就在静态存储区为每个多态类生成了一个type_info对象并初始化好__name等内容。它不是运行时 new 出来的。2.3type_info存在哪里放在只读数据段.rodata或类似段。每个多态类只有一份所有该类的对象共享同一个type_info实例。3. vtable 里的秘密type_info*藏在哪里3.1 Itanium ABI 的 vtable 布局在 GCC/Clang 使用的 Itanium C ABI 下vtable 的内存布局大致是这样vtable 指针指向这里 ──┐ ↓ ----------------------- | offset-to-top | (ptrdiff_t) ----------------------- | type_info* | ← RTTI 指针在这里 ----------------------- | Base::f | ← 虚函数地址 ----------------------- | Base::g | -----------------------offset-to-top用于dynamic_castvoid*找到对象完整起始地址type_info*就是 RTTI 的入口。3.2 为什么放在 vtable 里因为对象里只有一个 vptr。要找到type_info最经济的做法就是让 vptr 指向的 vtable 里包含这个指针。这样typeid(*p)→ 通过p-vptr找到 vtable → 取-1槽 → 拿到type_info*。dynamic_cast→ 同样先拿到type_info*再沿继承链搜索。3.3 MSVC 的差异MSVC 把type_info指针放在 vtable 的第 0 个槽位虚函数地址从第 1 个开始。所以“位置”是实现定义的但“vtable 里有 RTTI 指针”这一点是共通的。4.typeid的完整剖析4.1 两种行为模式typeid的行为取决于操作数是不是多态类型场景typeid(expr)的结果expr是非多态类型返回静态类型的type_info编译期就能确定expr是多态类型的左值对象或引用返回动态类型的type_info运行时通过 vtable 查询expr是多态类型的指针返回指针的静态类型即Base*不解引用struct Base { virtual ~Base() {} }; struct Derived : Base {}; Derived d; Base ref d; Base* ptr d; typeid(d) typeid(Derived); // true动态类型 typeid(ref) typeid(Derived); // true动态类型 typeid(ptr) typeid(Base*); // true指针的静态类型 typeid(*ptr) typeid(Derived); // true解引用后动态类型关键细节typeid对多态类型的指针本身不会去解引用查动态类型它只看指针的静态类型。只有对指针解引用*ptr或多态引用/对象才查动态类型。4.2typeid的底层实现伪代码逻辑如下// typeid(obj) 大致等价于 const std::type_info typeid_impl(const Base obj) { // obj 是多态类型编译器插入隐藏调用 return obj.__vptr[-1]; // 从 vtable 的 -1 槽取 type_info* }对于非多态类型编译器直接返回一个编译期确定的type_info对象的引用没有任何运行时开销。4.3 空指针的陷阱Base* p nullptr; typeid(*p); // 抛出 std::bad_typeid对空指针解引用后做typeid会抛异常因为没法从nullptr的“vtable”里取type_info。4.4type_info的比较type_info::operator在 Itanium ABI 下通常比较指针即可同一个类的type_info全局唯一。但标准不保证这一点跨 DSO动态库时可能出问题。5.dynamic_cast的完整剖析5.1 它到底在做什么dynamic_cast要做三件事确认转换是否合法目标类型是否在对象的动态类型继承链上。计算指针调整量多重继承下基类子对象和派生类对象地址不同。返回调整后的指针或失败信号。5.2 向下转型DowncastBase* p new Derived; Derived* d dynamic_castDerived*(p); // 成功底层步骤从p的 vtable 拿到type_info*假设是Derived的。检查Derived是否继承自Base目标类型。如果是计算Derived*相对Base*的偏移量返回调整后的指针。5.3 横向转型Crosscaststruct B1 { virtual ~B1() {} }; struct B2 { virtual ~B2() {} }; struct D : B1, B2 {}; D d; B1* p1 d; B2* p2 dynamic_castB2*(p1); // 横向转型成功B1*转B2*需要重新计算地址因为B2子对象在D中的偏移和B1不同。dynamic_cast会通过type_info里的继承关系信息找到正确的偏移。5.4type_info里到底存了什么Itanium ABI 的__class_type_info派生结构里__base_info数组存储了基类的type_info*和偏移量信息。dynamic_cast的核心算法就是从源类型的type_info开始。遍历基类列表递归查找目标类型。找到后沿路径累加偏移量得到最终指针。5.5 失败与异常Base* p new Base; Derived* d dynamic_castDerived*(p); // 返回 nullptr Base r *p; try { Derived d2 dynamic_castDerived(r); // 抛 std::bad_cast } catch (const std::bad_cast) { ... }指针版本返回nullptr引用版本抛std::bad_cast。5.6 为什么dynamic_cast慢线性搜索在复杂继承层次中需要遍历继承树。指针调整多重继承和虚继承需要计算偏移量。无法内联调用运行时库函数而非编译期展开。实测数据比static_cast慢60~200 倍。6. RTTI 的成本与禁用6.1 内存成本每个多态类多一份type_info对象里面存着类型名字符串。大型项目中继承层次复杂RTTI 数据可能显著膨胀。6.2 性能成本对象大小多态类多一个 vptr64 位下 8 字节。dynamic_cast运行时库调用 继承链搜索。虚函数间接调用无法内联。6.3-fno-rtti禁用编译时加-fno-rtti编译器不再生成 RTTI 数据。代价typeid和dynamic_cast不可用。多态异常catch基类引用捕获派生类异常不可用。必须全程序一致否则 ABI 不兼容。替代方案手动实现TypeId系统每个类定义一个静态 ID通过虚函数返回。7. 宏观总结一句话哲学RTTI 是 C 在“零开销抽象”原则下做的有条件妥协——你为多态付出了 vptr 的代价RTTI 只是顺便利用了这块 vptr把类型信息挂在 vtable 里。你用不用它vtable 都已经在那里了但一旦你用dynamic_cast就触发了运行时搜索的代价。8. 面试回答模板直接可用面试官问谈谈 C 的 RTTI。您好我从四个层面回答。第一本质。RTTI 是运行时类型识别核心工具是typeid和dynamic_cast。它只对多态类型真正生效——也就是至少有一个虚函数的类。非多态类型的typeid在编译期就确定了。第二底层实现。编译器为每个多态类生成一个std::type_info对象放在只读数据段每个类一份。这个对象的指针被塞进 vtable 的固定槽位——Itanium ABI 下是-1槽MSVC 是第 0 槽。typeid通过 vptr 找到 vtable再取type_info*dynamic_cast同样先拿type_info*然后遍历继承链搜索目标类型。第三dynamic_cast的细节。它要确认转换合法性并计算指针偏移量。单继承下偏移简单多重继承下需要从一个基类子对象地址调整到另一个基类子对象地址虚继承下偏移是运行时计算的。失败时指针返回nullptr引用抛bad_cast。性能上比static_cast慢 60~200 倍因为它是运行时库调用加线性搜索。第四代价与边界。RTTI 会增加二进制体积dynamic_cast有性能开销。很多项目用-fno-rtti禁用但会同时禁用dynamic_cast和多态异常。替代方案是手动实现 TypeId 系统。我的总结是RTTI 是 C 用少量内存和运行时开销换取类型安全向下转型的机制理解它的 vtable 存储方式就能在需要时做出正确的性能取舍。结语RTTI的学习暂时就到这里了这个东西的用处可以说其实就是识别对象实际类型的机制从而可以基于这个信息做安全转换。但它不只服务于转换异常匹配、类型映射、调试日志都会用到它。代价是 vptr 和 RTTI 数据带来的内存与性能开销所以是否启用、是否用dynamic_cast是一个需要权衡的工程决策。我是YYYing后面还有更精彩的内容希望各位能多多关注支持一下主包。无限进步我们下次再见---⭐️封面自取⭐️---