3

我最近在我的编译器 (MSVC10) 上禁用了 RTTI,并且可执行文件大小显着减小。通过使用文本编辑器比较生成的可执行文件,我发现无 RTTI 版本包含的符号名称要少得多,这说明了节省的空间。

AFAIK,这些符号名称仅用于填充type_info与每个多态类型关联的结构,并且可以通过编程方式调用它们来访问它们type_info::name()

根据标准,返回的字符串的格式type_info::name()是未指定的。也就是说,没有人可以依靠它便携地做严肃的事情。因此,实现应该有可能始终返回一个空字符串而不破坏任何内容,从而在不禁用 RTTI 支持的情况下减小可执行文件大小(因此我们仍然可以安全地使用typeid运算符 & comparetype_info的对象)。

但是……这可能吗?我正在使用 MSVC10,但没有找到任何选项。我可以完全禁用 RTTI ( /GR-),也可以使用完整的类型名称 ( /GR) 启用它。有没有编译器提供这样的选项?

4

3 回答 3

4

因此,实现应该有可能始终返回一个空字符串而不会破坏任何内容,从而在不禁用 RTTI 支持的情况下减小可执行文件大小(因此我们仍然可以使用 typeid 运算符并安全地比较 type_info 的对象)。

你误读了标准。type_info::name()从未指定(以空结尾的二进制字符串除外)生成返回值的目的是让编译器/库/运行时环境的实现者自由支配以实现他们认为最好的 RTTI 要求。作为程序员,您对如何设计或实现应用程序二进制接口(如果有)没有发言权。

于 2012-07-09T21:31:22.593 回答
3

你在这里问三个不同的问题。

  1. 最初的问题询问是否有任何方法可以让 MSVC 不生成名称,或者是否有可能与其他编译器一起使用,或者,如果失败了,是否有任何方法可以在不破坏任何东西的情况下从生成的 type_info 中删除名称。

  2. 然后您想知道是否可以修改 MS ABI(可能不会太激进),以便可以剥离名称。

  3. 最后,您想知道是否可以设计一个没有名称的 ABI。

问题 #1 本身就是一个复杂的问题。据我所知,没有办法让 MSVC 不生成名称。大多数其他编译器针对的是专门定义 typeid(foo).name() 必须返回的 ABI,因此它们也不能不生成名称。

更有趣的问题是,如果去掉名字会发生什么。对于 MSVC,我不知道答案。最好的办法可能是尝试一下——进入您的 DLL 并将每个名称的第一个字符更改为 \0 并查看它是否会破坏 dynamic_cast 等(我知道您可以使用 Mac 和 linux x86_64 可执行文件执行此操作由 g++ 4.2 生成并且它可以工作,但我们暂时把它放在一边。)

关于问题 #2,假设空白名称不起作用,将基于名称的系统修改为不再需要名称并不难。一个简单的解决方案是使用名称的散列,甚至是 ROT13 编码的名称(请记住,这里的原始目标是“我不希望普通用户看到我的类的令人尴尬的名称”)。但我不确定这是否适用于您正在寻找的东西。稍微复杂一点的解决方案如下:

  • 对于“dllexport”类,生成一个 UUID,将其放入 typeinfo,并将其放入与 DLL 一起生成的 .LIB 导入库中。
  • 对于“dllimport”ed 类,从 .LIB 中读取 UUID 并改用它。

因此,如果您设法正确获取 dllexport/dllimport,它将起作用,因为您的 exe 将使用与 dll 相同的 UUID。但如果你不这样做呢?如果您“不小心”在 DLL 和 EXE 中指定了相同的类(例如,具有相同参数的相同模板的实例化),而没有将一个标记为 dllexport 和一个标记为 dllimport,该怎么办?RTTI 不会将它们视为同一类型。

这是一个问题吗?好吧,C++ 标准并没有说它是。也没有任何 MS 文档。事实上,文档明确表示您不允许这样做。您不能在两个不同的模块中使用相同的类或函数,除非您明确地将其从一个模块导出并导入另一个模块。使用类模板很难做到这一点的事实是一个问题,而且这是一个他们没有尝试解决的问题。

让我们举一个现实的例子:创建一个基于节点的linkedlist类模板,其中包含一个全局静态哨兵,其中每个列表的最后一个节点都指向该哨兵,end() 函数只返回一个指向它的指针。(微软自己的 std::map 实现正是这样做的;我不确定这是否仍然正确。)linkedlist<int>在你的 exe 中新建 a ,并通过引用你的 dll 中试图从 to 迭代的函数来传递l.begin()l.end(). 它永远不会完成,因为 exe 创建的节点都不会指向 dll 中哨兵的副本。当然,如果你传递l.begin()l.end()进入 DLL,而不是传递l自身,你就不会有这个问题。您通常可以通过std::string或各种其他类型的引用,只是因为它们不依赖于任何破坏的东西。但你实际上并没有被允许这样做,你只是走运了。因此,虽然用必须在链接时查找的 UUID 替换名称意味着在链接加载器时无法匹配类型,但在链接加载器时类型已经无法匹配的事实意味着这是无关的。

建立一个没有这些问题的基于名称的系统是可能的。ARM C++ ABI (以及基于它的iOS 和 Android ABI)限制了程序员可以比 MS 少得多的东西,并且对链接加载器如何使其工作有非常具体的要求(3.2.5)。这个不能修改为不基于名称,因为它是设计中的明确选择:

• type_info::operator== 和 type_info::operator!= 比较 type_info::name() 返回的字符串,而不仅仅是指向 RTTI 对象及其名称的指针。

• 不依赖type_info::name() 返回的地址。(也就是说,t1.name() != t2.name() 并不意味着 t1 != t2)。

第一个条件有效地要求这些运算符(和 type_info::before())必须被调用,并且执行环境必须提供它们的适当实现。

但也可以构建一个不存在此问题且不使用名称的 ABI。这很好地延续到#3。

Itanium ABI (除其他外,在 x86_64 和 i386 上被OS X 和最近的 linux 使用)确实保证linkedlist<int>在一个对象中linkedlist<int>生成的 a 和从另一个对象中的相同标头生成的 a 可以在运行时链接在一起,并将成为相同类型,这意味着它们必须具有相同的 type_info 对象。从 2.9.1 开始:

当且仅当指针相等时,两个 type_info 指针才指向等价的类型描述。实现必须满足这个约束,例如通过使用符号抢占、COMDAT 部分或其他机制。

编译器、链接器和链接加载器必须协同工作,以确保在可执行文件中创建的 a指向与在共享对象linkedlist<int>中创建的完全相同的 type_info 对象。linkedlist<int>

所以,如果你只是把所有的名字都拿出来,那根本就没有任何区别。(这很容易测试和验证。)

但是你怎么可能实现这个 ABI 规范呢?j_kubik 有效地辩称这是不可能的,因为您必须在 .so 文件中保留一些链接时间信息。这指向了一个明显的答案:在 .so 文件中保留一些链接时间信息。事实上,您已经必须这样做来处理加载时重定位等问题;这只是扩展了您需要保留的内容。事实上,Apple 和 GNU/linux/g++/ELF 都是这样做的。(这也是几年前每个构建复杂 linux 系统的人都必须了解符号可见性和模糊链接的部分原因。)

有一个更明显的方法来解决这个问题:编写一个基于 C++ 的链接加载器,而不是试图让 C++ 编译器和链接器一起工作来欺骗基于 C 的链接加载器。但据我所知,自从 Be 之后没有人尝试过。

于 2012-07-10T23:47:07.167 回答
0

类型描述符的要求:

  • 在多编译单元和共享库环境中正常工作;
  • 适用于不同版本的共享库;
  • 工作正常,尽管不同的编译单元不共享任何关于类型的信息,除了它的名称:通常一个头文件用于所有编译单元来定义相同的类型,但这不是必需的;即使,它不会影响生成的目标文件。
  • 尽管模板实例化必须在使用它们的每个库中完全定义(因此包括 type_info 数据),但可以正常工作,并且如果一起使用多个此类库,则其行为类似于一种类型。

第四条规则本质上禁止所有基于非名称的类型描述符,如 UUID(除非在类型定义中特别提及,但这充其量只是名称替换,可能需要标准更改)。

在单独的文件(如建议 .LIB 文件)中存储 UUID 也会导致麻烦:实现新类型的不同库版本会导致麻烦。

编译单元应该能够共享相同的类型(及其type_info)而不需要涉及链接器 - 因为它应该不受任何语言细节的影响。

因此 type-name 只能是唯一的类型描述符,无需完全重新建模编译和链接(也是动态的)。我可以想象它可以工作,但不是在当前方案下。

于 2012-07-10T02:03:12.930 回答