假设我有三个编译对象,它们都是由同一编译器/版本生成的:
- A是使用C ++ 11标准编译的
- B用C ++ 14标准编译
- C用C ++ 17标准编译
为简单起见,我们假设所有标头均使用 C ++ 11编写,仅使用其语义在所有三个标准版本之间均未改变的构造,因此任何相互依赖关系都可以通过标头包含正确表达,并且编译器不反对。
这些对象是哪种组合,链接到单个二进制文件不安全吗?为什么?
编辑:涵盖主要编译器(例如gcc,clang,vs ++)的答案是受欢迎的
假设我有三个编译对象,它们都是由同一编译器/版本生成的:
为简单起见,我们假设所有标头均使用 C ++ 11编写,仅使用其语义在所有三个标准版本之间均未改变的构造,因此任何相互依赖关系都可以通过标头包含正确表达,并且编译器不反对。
这些对象是哪种组合,链接到单个二进制文件不安全吗?为什么?
编辑:涵盖主要编译器(例如gcc,clang,vs ++)的答案是受欢迎的
std::string
libstdc ++中的活动实现独立于所-std
使用的模式。这是一个重要属性,恰好支持OP等情况。您可以使用std::string
C ++ 03代码中的新代码,也可以使用std::string
C ++ 11代码中的旧代码(请参阅Matteo稍后注释中的链接)。
Answers:
这些对象是哪种组合,链接到单个二进制文件不安全吗?为什么?
对于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
版本的注释):
-std=c++03
-std=c++11
-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编译第四个对象G,-std=c++17
则需要重新编译具有相同版本的F(或不链接到F),因为F和G中的C ++ 17符号不兼容。
关于D,E和F之间上述兼容性的唯一警告是,您的程序必须使用libstdc++.so
GCC 7(或更高版本)中的共享库。由于对象F是使用GCC 7编译的,因此您需要使用该发行版中的共享库,因为使用GCC 7编译程序的任何部分可能会引入对libstdc++.so
GCC 4.9或GCC 5 中不存在的符号的依赖性。如果链接到使用GCC 8构建的对象G,则需要使用libstdc++.so
GCC 8中的from来确保找到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的配置方式,但是始终可以在编译时使用宏覆盖默认设置。
答案有两个部分。编译器级别的兼容性和链接器级别的兼容性。让我们从前者开始。
假设所有标头均使用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节:基本概念
更改:新的常规(非布局)解除分配器
理由:大量的重新分配是必需的。
对原始功能的影响:有效的C ++ 2011代码可以声明全局布局分配函数和释放函数,如下所示:
void operator new(std::size_t, std::size_t); void operator delete(void*, std::size_t) noexcept;
但是,在此国际标准中,运算符删除的声明可能与预定义的常规(非放置)运算符删除(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函数的有效C ++ 2011代码可能无法在此国际标准中进行编译。
C.4中讨论了C ++ 14和C ++ 17之间潜在的不兼容性。由于所有非标准头文件都是用C ++ 11编写的(如问题中所指定),因此不会发生这些问题,因此在此不再赘述。
现在,我将在链接器级别讨论兼容性。通常,不兼容的潜在原因包括:
main
入口点。如果生成的目标文件的格式取决于目标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 switch,它使您可以定位特定版本的C ++标准,但它不支持定位C ++ 11。可以指定的最低版本是C ++ 14,这是从Visual C ++ 2013 Update 3开始的默认版本。您可以使用旧版本的VC ++来定位C ++ 11,但是随后您必须使用其他VC ++编译器编译针对不同版本C ++标准的不同翻译单元,这至少会破坏WPO。
CAVEAT:我的回答可能不完整或非常精确。
新的C ++标准分为两个部分:语言功能和标准库组件。
正如您所说的那样,新标准本身就是语言本身的更改(例如,范围变更),几乎没有问题(有时在具有更新标准语言功能的第三方库标头中存在冲突)。
但是标准库...
每个编译器版本都附带一个C ++标准库的实现(带有gcc的libstdc ++,带有clang的libc ++,带有VC ++的MS C ++标准库...)以及一个实现,每个标准版本的实现不多。同样,在某些情况下,您可以使用标准库的其他实现方式,而不是提供的编译器。您应该关心的是将较旧的标准库实现与较新的实现链接。
第三方库与您的代码之间可能发生的冲突是链接到该第三方库的标准库(和其他库)。