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

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

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

为了简单起见,我们假设所有的头都是用 C + + 11、 只使用其语义在所有三个标准版本之间没有变化的结构编写的,因此任何相互依赖性都可以通过头包含正确地表达出来,而且编译器不会反对。

这些对象的组合是什么? 链接到一个单一的二进制对象不安全吗? 为什么?


编辑: 欢迎回答包括主要编译器(例如,gcc,clang,vs + +)

26782 次浏览

新的 C + + 标准由两部分组成: 语言特性和标准库组件。

正如您所说的 新标准,语言本身的变化(例如 range-for)几乎没有问题(有时在具有新的标准语言特性的第三方库头中存在冲突)。

但是标准图书馆..。

每个编译器版本都有一个 C++标准程式库的实现(使用 gcc 的 libstdc + + ,使用 clang 的 libc + + ,使用 VC + + 的 MS C++标准程式库,... ...) ,只有一个实现,每个标准版本没有多少实现。此外,在某些情况下,您可以使用其他标准库的实现而不是编译器提供的。您应该关心的是将较旧的标准库实现与较新的标准库实现链接起来。

第三方库和代码之间可能发生的冲突是链接到第三方库的标准库(和其他库)。

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

让我们假设所有的头都是用 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 + + 14/17的翻译单元出现令人惊讶的错误。因此,即使头文件是用 C + + 11编写的,也必须小心地编写,以确保它与标准的后续版本兼容。__cplusplus指令在这里可能有用。

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

C. 3.2第3条: 基本概念

Change : 新的常用(非放置)释放分配器

基本原理 : 大小释放需要。

对原始特性的影响 : 有效的 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条: 声明

Change : Constexpr 非静态成员函数不是隐式 const 成员函数成员函数。

基本原理 : 允许 Constexpr 成员函数对 对象。

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

例如,下面的代码在 C + + 2011中有效,但在 这个国际标准,因为它声明同一个成员 使用不同的返回类型运行两次:

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

C. 3.4子句27: 输入/输出库

Change : gets 没有定义。

基本原理 : 使用 gets 被认为是危险的。

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

C + + 14和 C + + 17之间潜在的不兼容性在 C 4中进行了讨论。因为所有的非标准头文件都是用 C + + 11编写的(如问题中所指定的) ,所以这些问题不会发生,所以我不会在这里提到它们。

现在我将讨论链接器级别的兼容性:

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

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

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

一般来说,WPO 可能无法很好地处理使用不同 C + + 标准编译的目标文件。这取决于编译器的哪些阶段需要了解目标标准,哪些阶段不需要,以及它对跨对象文件的过程间优化的影响。幸运的是,GCC、 LLVM 和 VC + + 的设计都很好,没有这个问题(据我所知没有)。

因此,GCC、 LLVM 和 VC + + 的设计就是为了支持不同版本的 C + + 标准之间的 二进制兼容性。但这并不是标准本身的要求。

顺便说一下,尽管 VC + + 编译器提供了 标准开关,它允许您针对 C + + 标准的特定版本,但是它不支持针对 C + + 11。可以指定的最小版本是 C + + 14,这是从 VisualC + + 2013Update3开始的默认版本。你可以使用旧版本的 VC + + 来针对 C + + 11,但是你必须使用不同的 VC + + 编译器来编译不同的翻译单元来针对不同版本的 C + + 标准,这至少会破坏 WPO。

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

这些对象的组合是什么? 链接到一个单一的二进制对象不安全吗? 为什么?

对于 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特性之间存在不兼容的变化。类似地,如果你用 GCC7和 -std=c++17编译一个对象,用 GCC8和 -std=c++17编译另一个对象,你将会遇到问题,因为 GCC7和8中的 C + + 17支持仍然处于试验阶段,并在不断发展。

另一方面,以下对象的任何组合都可以工作(不过请参阅下面关于 libstdc++.so版本的说明) :

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

这是因为 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 和 G 中的 C + + 17符号是不兼容的。

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

在使用 GCC 时需要注意的另一点是,自从 GCC 5以来,libstdc + + 中就有了 std::string的两个实现。这两个实现不是链接兼容的(它们有不同的错误名称,所以不能链接在一起) ,但是可以共存在同一个二进制文件中(它们有不同的错误名称,所以如果一个对象使用 std::string而另一个使用 std::__cxx11::string,不要冲突)。如果您的对象使用 std::string,那么通常它们都应该使用相同的字符串实现进行编译。用 -D_GLIBCXX_USE_CXX11_ABI=0编译来选择原来的 gcc4-compatible实现,或者用 -D_GLIBCXX_USE_CXX11_ABI=1来选择新的 cxx11实现(不要被名称所迷惑,它也可以在 C + + 03中使用,它被称为 cxx11是因为它符合 C + + 11的要求)。哪个实现是默认实现取决于 GCC 的配置方式,但是默认实现总是可以在编译时用宏重写。