链接 C 17、C 14 和 C 11 对象是否安全

新手上路,请多包涵

假设我有三个编译对象,都由 相同的编译器/版本 生成:

  1. A 使用 C++11 标准编译
  2. B 使用 C++14 标准编译
  3. C 是使用 C++17 标准编译的

为简单起见,我们假设所有头文件都是用 C++11 编写的, _仅使用语义在所有三个标准版本之间都没有改变的结构_,因此任何相互依赖都可以通过头文件包含正确表达,并且编译器没有反对。

这些对象的哪些组合是它,链接到单个二进制文件是否安全?为什么?


编辑:欢迎回答涵盖主要编译器(例如 gcc、clang、vs++)

原文由 ricab 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 2k
2 个回答

这些对象的哪些组合是它,链接到单个二进制文件是否安全?为什么?

对于 GCC ,将对象 A、B 和 C 的任意组合链接在一起是安全的。如果它们都使用相同的版本构建,那么它们是 ABI 兼容的,标准版本(即 -std 选项)不没什么区别。

为什么?因为这是我们实施的一个重要属性,我们努力确保这一点。

如果您将使用不同版本的 GCC 编译的对象链接在一起, 并且 在 GCC 对该标准的支持完成之前使用了新 C++ 标准的不稳定功能,那么您遇到的问题是。例如,如果您使用 GCC 4.9 和 -std=c++11 编译一个对象,而另一个使用 GCC 5 和 -std=c++11 的对象会出现问题。 C++11 支持在 GCC 4.x 中是实验性的,因此 GCC 4.9 和 5 版本的 C++11 特性之间存在不兼容的变化。同样,如果你用 GCC 7 和 -std=c++17 编译一个对象,用 GCC 8 和 -std=c++17 编译另一个对象,你会遇到问题,因为 GCC 7 和 8 中的 C++17 支持仍然存在实验和发展。

另一方面,以下对象的任何组合都将起作用(尽管请参阅下面关于 libstdc++.so 版本的注释):

  • 使用 GCC 4.9 编译的对象 D 和 -std=c++03
  • 使用 GCC 5 编译的对象 E 和 -std=c++11
  • 使用 GCC 7 编译的对象 F 和 -std=c++17

这是因为 C++03 支持在使用的所有三个编译器版本中都是稳定的,因此 C++03 组件在所有对象之间是兼容的。自 GCC 5 起,C++11 支持稳定,但对象 D 不使用任何 C++11 功能,对象 E 和 F 都使用 C++11 支持稳定的版本。 C++17 支持在任何使用的编译器版本中都不稳定,但只有对象 F 使用 C++17 特性,因此与其他两个对象没有兼容性问题(它们共享的唯一特性来自 C++03或 C++11,并且使用的版本使这些部分正常)。如果您稍后想使用 GCC 8 和 -std=c++17 编译第四个对象 G,那么您需要使用相同版本(或不链接到 F)重新编译 F,因为 F 中的 C++17 符号和 G 不兼容。

对于上述 D、E 和 F 之间的兼容性,唯一需要注意的是您的程序必须使用来自 GCC 7(或更高版本)的 libstdc++.so 共享库。因为对象 F 是使用 GCC 7 编译的,所以您需要使用该版本中的共享库,因为使用 GCC 7 编译程序的任何部分可能会引入对来自 GCC 的 libstdc++.so 中不存在的符号的依赖关系4.9 或 GCC 5。同样,如果您链接到使用 GCC 8 构建的对象 G,则需要使用 GCC 8 中的 libstdc++.so 以确保找到 G 所需的所有符号。简单的规则是确保程序在运行时使用的共享库至少与用于编译任何对象的版本一样新。

使用 GCC 时的另一个警告(在您的问题的评论中已经提到)是,由于 GCC 5,libstdc++ 中有 两种实现 std::string 可用。这两个实现不是链接兼容的(它们具有不同的重整名称,因此不能链接在一起)但可以共存于同一个二进制文件中(它们具有不同的重整名称,因此如果一个对象使用不要冲突 std::string 和其他用途 std::__cxx11::string )。如果您的对象使用 std::string 那么通常它们都应该使用相同的字符串实现进行编译。 Compile with -D_GLIBCXX_USE_CXX11_ABI=0 to select the original gcc4-compatible implementation, or -D_GLIBCXX_USE_CXX11_ABI=1 to select the new cxx11 implementation (don’t be fooled by the名字,也可以在C++03中使用,叫 cxx11 因为它符合C++11的要求)。哪个实现是默认的取决于 GCC 的配置方式,但默认值总是可以在编译时用宏覆盖。

原文由 Jonathan Wakely 发布,翻译遵循 CC BY-SA 3.0 许可协议

答案分为两部分。编译器级别的兼容性和链接器级别的兼容性。让我们从前者开始。

假设所有头文件都是用 C++11 编写的

使用相同的编译器意味着无论目标 C++ 标准如何,都将使用相同的标准库头文件和源文件(与编译器关联的文件)。因此,标准库的头文件被编写为与编译器支持的所有 C++ 版本兼容。

也就是说,如果用于编译翻译单元的编译器选项指定了特定的 C++ 标准,那么仅在较新标准中可用的任何功能都不应访问。这是使用 __cplusplus 指令完成的。有关如何使用它的有趣示例,请参阅 矢量 源文件。同样,编译器将拒绝新版本标准提供的任何语法特性。

所有这一切意味着您的假设只能适用于您编写的头文件。当这些头文件包含在针对不同 C++ 标准的不同翻译单元中时,可能会导致不兼容。这在 C++ 标准的附录 C 中进行了讨论。有4个从句,我只讨论第一个,其余的简单提一下。

C.3.1 第 2 条:词汇约定

单引号在 C++11 中分隔字符文字,而在 C++14 和 C++17 中它们是数字分隔符。假设您在纯 C++11 头文件之一中有以下宏定义:

 #define M(x, ...) __VA_ARGS__

// Maybe defined as a field in a template or a type.
int x[2] = { M(1'2,3'4) };

考虑两个包含头文件的翻译单元,但分别以 C++11 和 C++14 为目标。面向 C++11 时,引号内的逗号不被视为参数分隔符;只有一次参数。因此,代码将等效于:

 int x[2] = { 0 }; // C++11

另一方面,当面向 C++14 时,单引号被解释为数字分隔符。因此,代码将等效于:

 int x[2] = { 34, 0 }; // C++14 and C++17

这里的要点是,在纯 C++11 头文件之一中使用单引号可能会导致以 C++1417 为目标的翻译单元中出现令人惊讶的错误。因此,即使头文件是用 C++11 编写的,也必须仔细编写以确保它与标准的更高版本兼容。 __cplusplus 指令在这里可能有用。

该标准的其他三个条款包括:

C.3.2 第 3 条:基本概念

更改:新的通常(非放置)释放器

基本原理:大小释放所必需的。

对原始功能的影响:有效的 C++2011 代码可以声明一个全局布局分配函数和释放函数,如下所示:

 void operator new(std::size_t, std::size_t);
void operator delete(void*, std::size_t) noexcept;

然而,在本国际标准中,运算符 delete 的声明可能与预定义的常用(非放置)运算符 delete (3.7.4) 匹配。如果是这样,则程序格式错误,就像类成员分配函数和释放函数 (5.3.4) 一样。

C.3.3 第 7 条:声明

更改: constexpr 非静态成员函数不是隐式 const 成员函数。

基本 原理:必须允许 constexpr 成员函数改变对象。

对原始功能的影响:有效的 C++2011 代码可能无法在本国际标准中编译。

例如,以下代码在 C++2011 中有效,但在本国际标准中无效,因为它以不同的返回类型声明了两次相同的成员函数:

 struct S {
constexpr const int &f();
int &f();
};

C.3.4 第 27 条:输入/输出库

更改:未定义gets。

理由:使用gets 被认为是危险的。

对原始功能的影响:使用gets 函数的有效C++2011 代码可能无法在本国际标准中编译。

C.4 中讨论了 C++14 和 C++17 之间潜在的不兼容性。由于所有的非标准头文件都是用C++11编写的(如问题中所述),不会出现这些问题,所以这里不再赘述。

现在我将讨论链接器级别的兼容性。通常,不兼容的潜在原因包括以下内容:

  • 目标文件的格式。
  • 程序启动和终止例程以及 main 入口点。
  • 整个程序优化(WPO)。

如果生成的目标文件的格式取决于目标 C++ 标准,则链接器必须能够链接不同的目标文件。幸运的是,在 GCC、LLVM 和 VC++ 中,情况并非如此。也就是说,无论目标标准如何,目标文件的格式都是相同的,尽管它高度依赖于编译器本身。事实上,GCC、LLVM 和 VC++ 的链接器都不需要有关目标 C++ 标准的知识。这也意味着我们可以链接已经编译好的目标文件(静态链接运行时)。

如果程序启动例程(调用 main 的函数)对于不同的 C++ 标准是不同的,并且不同的例程彼此不兼容,则无法链接目标文件。幸运的是,在 GCC、LLVM 和 VC++ 中,情况并非如此。此外, main 函数的签名(以及适用于它的限制,参见标准的第 3.6 节)在所有 C++ 标准中都是相同的,因此它在哪个翻译单元中并不重要存在。

通常,WPO 可能不适用于使用不同 C++ 标准编译的目标文件。这取决于编译器的哪些阶段需要了解目标标准,哪些阶段不需要,以及它对跨目标文件的过程间优化的影响。幸运的是,GCC、LLVM 和 VC++ 设计良好,没有这个问题(我不知道)。

因此,GCC、LLVM 和 VC++ 旨在实现跨不同版本的 C++ 标准的 二进制 兼容性。不过,这并不是标准本身的真正要求。

顺便说一句,尽管 VC++ 编译器提供了 std 开关,它使您能够以特定版本的 C++ 标准为目标,但它不支持以 C++11 为目标。可以指定的最低版本是 C++14,这是从 Visual C++ 2013 Update 3 开始的默认版本。您可以使用旧版本的 VC++ 来定位 C++11,但是您必须使用不同的 VC++ 编译器编译针对不同版本的 C++ 标准的不同翻译单元,这至少会破坏 WPO。

警告:我的回答可能不完整或非常精确。

原文由 Hadi Brais 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题