link C++17、C++14 和 C++11 objects 安全吗

Is it safe to link C++17, C++14, and C++11 objects

假设我有三个编译的 objects,全部由 same compiler/version:

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

为简单起见,我们假设所有 header 都是用 C++11 编写的,仅使用语义在所有三个标准版本之间没有改变的结构,因此,任何相互依赖性都可以通过包含 header 正确表达,而编译器不会 object.

这些 object 的哪些组合是 link 到单个二进制文件中是安全的还是不安全的?为什么?


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

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

正如您所说的新标准,语言本身的变化(例如ranged-for)几乎没有问题(有时在第 3 方库头文件中与更新的文件存在冲突标准语言功能)。

但是标准库...

每个编译器版本都带有一个 C++ 标准库的实现(带有 gcc 的 libstdc++,带有 clang 的 libc++,带有 VC++ 的 MS C++ 标准库,...)并且只有一个实现,实现不多对于每个标准版本。同样在某些情况下,您可能会使用标准库的其他实现而不是编译器提供的。您应该关心的是将较旧的标准库实现与较新的标准库实现链接起来。

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

答案分为两部分。编译器级别的兼容性和 linker 级别的兼容性。先从前者说起。

let's assume all headers were written in C++11

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

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

所有这些意味着您的假设只能适用于您编写的头文件。当包含在针对不同 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: New usual (non-placement) deallocator

Rationale: Required for sized deallocation.

Effect on original feature: Valid C++2011 code could declare a global placement allocation function and deallocation function as follows:

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

In this International Standard, however, the declaration of operator delete might match a predefined usual (non-placement) operator delete (3.7.4). If so, the program is ill-formed, as it was for class member allocation functions and deallocation functions (5.3.4).

C.3.3 第 7 条:声明

Change: constexpr non-static member functions are not implicitly const member functions.

Rationale: Necessary to allow constexpr member functions to mutate the object.

Effect on original feature: Valid C++2011 code may fail to compile in this International Standard.

For example, the following code is valid in C++2011 but invalid in this International Standard because it declares the same member function twice with different return types:

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

C.3.4 第 27 条:input/output 图书馆

Change: gets is not defined.

Rationale: Use of gets is considered dangerous.

Effect on original feature: Valid C++2011 code that uses the gets function may fail to compile in this International Standard.

C.4 中讨论了 C++14 和 C++17 之间的潜在不兼容性。由于所有的non-standard头文件都是用C++11写的(问题中有说明),所以不会出现这些问题,这里就不提了。

现在我将讨论 linker 级别的兼容性。一般来说,不兼容的潜在原因包括:

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

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

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

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

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

注意:我的回答可能不完整或不准确。

Which combinations of these objects is it and isn't it safe to link into a single binary? Why?

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

为什么?因为这是我们努力确保实现的重要 属性。

如果您 link 将使用不同版本的 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 和 -std=c++03
  • 编译的对象 D
  • 用 GCC 5 和 -std=c++11
  • 编译的对象 E
  • 用 GCC 7 和 -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,那么您需要使用相同的版本(或者不是 link 到 F)重新编译 F,因为 C++17 符号在 F 和 G 中不兼容。

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

使用 GCC 时的另一个注意事项(已在您的问题的评论中提到)是,自 GCC 5 以来,libstdc++ 中有 std::string 的两个实现。这两个实现不是 link-compatible(它们有不同的错位名称,所以不能 link 在一起)但可以 co-exist 在同一个二进制文件中(它们有不同的错位名称,所以不要如果一个对象使用 std::string 而另一个对象使用 std::__cxx11::string,则不会发生冲突)。如果您的对象使用 std::string 那么通常它们都应该使用相同的字符串实现进行编译。将 -D_GLIBCXX_USE_CXX11_ABI=0 编译为 select 原始 gcc4-compatible 实现,或将 -D_GLIBCXX_USE_CXX11_ABI=1 编译为 select 新的 cxx11 实现(不要被name,它也可以在C++03中使用,它被称为cxx11是因为它符合C++11的要求)。哪个实现是默认的取决于 GCC 的配置方式,但默认总是可以在 compile-time 处用宏覆盖。