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


97

假设我有三个编译对象,它们都是由同一编译器/版本生成的

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

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

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


编辑:涵盖主要编译器(例如gcc,clang,vs ++)的答案是受欢迎的


6
不是学校/面试问题。这个问题源于一个特定的案例:我正在开发一个依赖于开源库的项目。我从源代码构建此库,但是其构建系统仅接受在C ++ 03 / C ++ 11构建之间进行选择的标志。我使用的编译器支持其他标准,但是我正在考虑将自己的项目升级到C ++ 17。我不确定这是否是安全的决定。ABI是否会中断或不建议采用其他方法?我没有找到明确的答案,因此决定发布有关一般案件的问题。
ricab

6
这完全取决于编译器。正式的C ++规范中没有任何内容可以控制这种情况。用C ++ 03或C + 11标准编写的代码在C ++ 14和C ++ 17级别上也有一些问题的可能性很小。有了足够的知识和经验(以及编写良好的代码开始),就有可能解决所有这些问题。但是,如果您对新的C ++标准不是很熟悉,则最好坚持构建系统所支持的功能,并经过测试可以使用。
Sam Varshavchik '17

9
@Someprogrammerdude:这是一个非常值得的问题。我希望我有一个答案。我所知道的是,通过RHEL devtoolset进行的libstdc ++在设计上是向后兼容的,方法是静态地链接较新的内容,而使较旧的内容使用发行版的“本机” libstdc ++在运行时动态解析。但这并不能回答问题。
Lightness Races in Orbit

3
@nm:...多数情况下...几乎每个分发独立于发行版的C ++库的人都这样做(1)以动态库的形式,(2)在接口边界上没有C ++标准库的容器。来自Linux发行版的库很容易,因为它们都是使用相同的编译器,相同的标准库和几乎相同的默认标志集构建的。
Matteo Italia,

3
只是为了阐明@MatteoItalia的早期注释“以及从C ++ 03切换到C ++ 11模式时(特别是std :: string)。” 这是不正确的,std::stringlibstdc ++中的活动实现独立于所-std使用的模式。这是一个重要属性,恰好支持OP等情况。您可以使用std::stringC ++ 03代码中的新代码,也可以使用std::stringC ++ 11代码中的旧代码(请参阅Matteo稍后注释中的链接)。
乔纳森·威克利

Answers:


116

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

对于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和 -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编译第四个对象G,-std=c++17则需要重新编译具有相同版本的F(或不链接到F),因为F和G中的C ++ 17符号不兼容。

关于D,E和F之间上述兼容性的唯一警告是,您的程序必须使用libstdc++.soGCC 7(或更高版本)中的共享库。由于对象F是使用GCC 7编译的,因此您需要使用该发行版中的共享库,因为使用GCC 7编译程序的任何部分可能会引入对libstdc++.soGCC 4.9或GCC 5 中不存在的符号的依赖性。如果链接到使用GCC 8构建的对象G,则需要使用libstdc++.soGCC 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的配置方式,但是始终可以在编译时使用宏覆盖默认设置。


“因为使用GCC 7编译程序的任何部分可能会引入对GCC 4.9或GCC 5的libstdc ++。so中存在的符号的依赖性”,您的意思是GCC 4.9或GCC 5所不存在的符号,对吗?这也适用于静态链接吗?感谢您提供有关跨编译器版本兼容性的信息。
哈迪·布雷斯

1
我刚刚意识到在提供悬赏这个问题上存在巨大缺陷。😂
亮度种族在轨道

4
@ricab我很确定90%的答案对于Clang / libc ++来说是相同的,但是我对MSVC不了解。
乔纳森·威基利

1
这个答案很出色。是否有记录表明5.0+对于11/14稳定?
巴里(Barry)'18年

1
不太清楚或在一个地方。gcc.gnu.org/gcc-5/changes.html#libstdcxxgcc.gnu.org/onlinedocs/libstdc++/manual/api.html#api.rel_51声明对C ++ 11的库支持已完成(该语言支持功能较早,但仍然是“实验性的”)。直到6.1为止,C ++ 14库支持仍被列为试验性的,但我认为在实践中5.x和6.x之间没有任何变化会影响ABI。
乔纳森·威基利

16

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

假设所有标头均使用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入口点。
  • 整个程序优化(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 switch,它使您可以定位特定版本的C ++标准,但它不支持定位C ++ 11。可以指定的最低版本是C ++ 14,这是从Visual C ++ 2013 Update 3开始的默认版本。您可以使用旧版本的VC ++来定位C ++ 11,但是随后您必须使用其他VC ++编译器编译针对不同版本C ++标准的不同翻译单元,这至少会破坏WPO。

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


这个问题的实质是关注链接而不是编译。我意识到(由于此评论)可能不清楚,并对其进行了编辑以使所有三个标准中包含的头文件都具有相同的解释。
ricab

@ricab答案涵盖了编译和链接。我以为你在问这两个问题。
哈迪·布雷斯

1
确实,但是我发现答案太长且令人困惑,尤其是在“现在我将在链接器级别讨论兼容性”之前。您可以将上面的所有内容替换为类似的内容,例如,如果不能假设所包含的标头在C ++ 11和C ++ 14/17中具有相同的含义,那么首先将它们包括在内是不安全的。对于其余部分,您是否有资料显示这三个要点是造成不兼容的唯一潜在原因?无论如何,感谢您的回答,我仍在投票
ricab

@ricab我不能肯定地说。这就是为什么我在答案的末尾添加了警告。如果我错过了什么,欢迎其他任何人扩大答案以使其更加准确或完整。
哈迪·布雷斯

这使我感到困惑:“使用相同的编译器意味着将使用相同的标准库头和源文件(...)”。怎么会这样呢?如果我有使用gcc5编译的旧代码,则该版本的“编译器文件”不能作为将来的证明。对于用不同的编译器版本在(不同时间)不同时间编译的源代码,我们可以确定库头和源文件是不同的。按照这些规则应该相同的规则,您必须使用gcc5重新编译较旧的源代码,并确保它们都使用最新的(相同的)“编译器文件”。
user2943111

2

新的C ++标准分为两个部分:语言功能和标准库组件。

正如您所说的那样,新标准本身就是语言本身的更改(例如,范围变更),几乎没有问题(有时在具有更新标准语言功能的第三方库标头中存在冲突)。

但是标准库...

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

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


“每个编译器版本都带有STL的实现”不,它们不是
Lightness Races in Orbit

@LightnessRacesinOrbit您是说libstdc ++和gcc之间没有关系吗?
E. Vakili

8
不,我的意思是STL已经过时二十多年了。您的意思是C ++标准库。至于其余的答案,您能否提供一些参考/证据来支持您的主张?我认为对于这样的问题很重要。
Lightness Races in Orbit

3
抱歉,不,文字内容不清楚。您已经做出了一些有趣的断言,但是还没有任何证据支持它们。
Lightness Races in Orbit
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.