C++编程C++ 开发人员

分析特定的运行时机制,使得 **std::type_index** 能够在不同翻译单元中建立对 **std::type_info** 对象的完全排序,即使不同的静态存储实例表示相同的类型?

用 Hintsage AI 助手通过面试

问题的答案

std::type_index 通过封装指向 std::type_info 对象的指针,并将比较委托给基础的 before() 成员函数,实现跨翻译单元的排序。C++ ABI 强制要求链接器将相同类型的类型信息合并为一个规范对象(使用 COMDAT 段或弱符号),或者确保 before() 提供一致的完全排序,而不论物理地址差异。因此,std::type_index 仅仅封装了这种 ABI 保证的比较,提供 operator< 和哈希支持,而不要求类型在比较时是完整的。该机制完全依赖于运行时类型信息 (RTTI) 被启用,因为编译器必须发出类型元数据,以便链接器能够在共享库边界中去重或调和类型身份。

生活中的情况

问题描述

在为游戏引擎设计插件架构时,我们需要一个中央注册表,将组件类型映射到工厂函数。每个插件(共享库)使用 typeid(Component).name() 注册其组件作为键。然而,在跨平台测试中,我们发现,当一个插件在一个共享库中加载时,尝试检索由核心引擎在另一个共享库中注册的工厂时,std::map 查找间歇性失败。根本原因是 type_info::name() 返回的字符串名称在编译器之间(GCC 与 Clang)不同,而直接比较 type_info 对象的指针失败,因为每个共享库都包含同一类型的不同静态实例。

考虑的解决方案

解决方案 1:手动字符串规范化

我们考虑使用编译器特定的 API(例如 abi::__cxa_demangle)来解码和规范化 type_info::name() 字符串,以创建规范键。这种方法可以创建适合调试的人类可读标识符。

优点: 人类可读的键便于日志记录和序列化。

缺点: 解码耗费资源,字符串比较比整数比较慢,而且格式仍然取决于实现,未来编译器的升级可能打破注册表。

解决方案 2:虚拟继承和自定义 RTTI

我们探索了要求所有组件从提供虚拟 GetTypeID() 方法的基类继承,该方法返回手动分配的整数常量。

优点: 确定性、快速的整数比较,不依赖编译器 RTTI。

缺点: 手动 ID 分配容易出错(冲突),需要修改类层次结构,无法处理不在我们控制范围内的第三方类型。

解决方案 3:采用 std::type_index

我们重构了注册表,以使用 std::map<std::type_index, FactoryFunc>,利用 std::type_index(typeid(T)) 作为键。

优点: 标准保证通过符合 ABI 的 type_info 比较在翻译单元之间提供一致的排序和哈希,不需要手动管理 ID,并且与现有使用 typeid 的代码无缝集成。

缺点: 需要启用 RTTI(增加二进制大小),而 type_index 对象不能序列化以进行网络传输或持久存储。

选择的解决方案

我们选择了 解决方案 3,因为跨库的类型识别的可靠性超过了 RTTI 的二进制大小成本。标准强制规定的 std::type_index 行为消除了脆弱的字符串解析和手动 ID 维护,这些问题困扰着其他解决方案。

结果

注册表在 Linux、Windows 和 macOS 的 DLL 边界上正常工作。工厂查找变成了 O(log N) 的内部指针比较,而不是字符串操作,与解码方法相比,组件实例化延迟大约减少了 40%。该系统现在支持热重载插件,无需重新注册核心引擎类型。

候选人常常忽视的内容

为什么 std::type_index::name() 在不同编译器版本中对同一类型产生不同的输出,为什么它不适合用作持久存储键?

std::type_info::name() 返回一个实现定义的以空字符结尾的字节字符串;C++ 标准明确拒绝指定其格式、编码或稳定性。例如,GCC 通常返回编码名称(例如,"St6vectorIiSaIiEE"),而 MSVC 返回人类可读的名称(例如,"class std::vector<int,class std::allocator<int> >")。编译器供应商可能会在未来版本中更改这些表示,以改善调试或减少符号长度。因此,将这些字符串序列化到磁盘或网络协议中,在编译器升级时会造成未定义行为,因为之前保存的键将不再与新生成的键匹配。候选人常常错误地认为 name() 的行为像稳定的 UUID。

当使用 -fno-rtti 编译时,std::type_index 的行为如何,为什么这会触发编译错误而不是运行时异常?

当 RTTI 被禁用时,编译器不会为多态类型发出 type_info 对象,typeid 操作符变为不良构造(除了静态类型的表达式外,它在某些实现中返回静态类型信息,但通常是禁用的)。std::type_index 需要一个 const std::type_info& 进行构造,而没有 RTTI,二进制中不存在必要的类型元数据。由于这是对生成的元数据的编译时依赖性,因此编译器在链接期间发出错误(例如,"undefined reference to typeinfo for X"),而不是推迟到可捕获的运行时异常。候选人常常期待运行时的 std::bad_typeid 或类似内容,将其与 dynamic_cast 失败混淆。

哪种特定限制阻止 std::type_index 被用作非类型模板参数 (NTTP),这与 typeid 的 constexpr 评估有什么关系?

std::type_index 在内部存储一个指向 std::type_info 对象的指针(或引用)。在 C++20 及之前版本中,非类型模板参数需要结构类型,其中所有成员都是公共的并且是结构类型(或其数组),并且它们不能包含指向具有动态存储或链接依赖地址的对象的指针。由于 type_info 对象驻留在静态存储中,具有链接器依赖的地址,而 std::type_index 不是结构类型(在某些实现中它有私有成员和非平凡的复制构造函数),因此无法用作 NTTP。尽管 C++23 允许在常量表达式中使用 typeid,但 std::type_index 本身在大多数标准库实现中仍然不是文字型或非结构型,阻止它在需要编译时常量的模板参数中使用。