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:
生成
- A 是使用 C++11 标准编译的
- B 是使用 C++14 标准编译的
- 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 级别的兼容性。一般来说,不兼容的潜在原因包括:
- 目标文件的格式。
- 程序启动和终止例程以及
main
入口点。
- Whole program optimization (WPO).
如果生成的目标文件的格式取决于目标 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 处用宏覆盖。
假设我有三个编译的 objects,全部由 same compiler/version:
生成- A 是使用 C++11 标准编译的
- B 是使用 C++14 标准编译的
- 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 级别的兼容性。一般来说,不兼容的潜在原因包括:
- 目标文件的格式。
- 程序启动和终止例程以及
main
入口点。 - Whole program optimization (WPO).
如果生成的目标文件的格式取决于目标 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 处用宏覆盖。