是否需要在同一环境中使用同一编译器对同一程序进行编译之间保持一致的未指定和未定义行为?

Posted

技术标签:

【中文标题】是否需要在同一环境中使用同一编译器对同一程序进行编译之间保持一致的未指定和未定义行为?【英文标题】:Are unspecified and undefined behavior required to be consistent between compiles of the same program with the same compiler in the same environment? 【发布时间】:2011-03-04 20:19:45 【问题描述】:

让我们假设我的程序包含 C++ 标准规定为未指定行为的特定构造。这基本上意味着实现必须做一些合理的事情,但允许不记录它。但是,每次编译具有未指定行为的特定构造时,实现是否需要产生相同的行为,还是允许在不同的编译中产生不同的行为?

未定义的行为呢?假设我的程序包含一个符合标准的 UB 结构。允许实现展示任何行为。但是,在相同环境中具有相同设置的相同编译器上的相同程序的编译之间,这种行为会有所不同吗?换句话说,如果我在文件 X.cpp 的第 78 行取消引用一个空指针,并且在这种情况下实现对驱动器进行格式化,这是否意味着它会在重新编译程序后执行相同的操作?

问题是……我用相同的编译器在相同的环境中使用相同的编译器设置来编译相同的程序。声明为未指定行为和未定义行为的构造会在每次编译时产生相同的行为,还是允许它们在编译之间有所不同?

【问题讨论】:

【参考方案1】:

如果它是未定义的行为,那么本质上将发生的事情是未定义的,您不能指望它在任何情况下都是相同的。

另一方面,未指定的行为由各个供应商决定如何实施,例如,如果语言规范中存在歧义。这在编译和运行之间是一致的,但在不同的供应商之间不一定是一致的。因此,例如,当您仅使用 Visual Studio 构建时依赖未指定的行为是可以的,但如果您尝试将代码移植到 gcc,它可能会失败或产生与您预期不同的行为。

【讨论】:

你只回答了一半的问题。未指明的行为呢? :) 我认为未指定的行为也不需要任何形式的一致性。我相信展开调用函数的循环的编译器可能具有例如如果这样做会改善寄存器分配,则第一次或最后一次通过循环以不同的顺序评估参数。【参考方案2】:

未定义的行为在同一程序的运行之间可能会有所不同,甚至在同一程序运行中相同代码的执行之间也会有所不同。例如,未初始化(自动)变量的值是未定义的,然后它的实际值就是恰好在内存中那个位置的任何值。显然,这可能会有所不同。

编辑:

这也适用于未指定的行为。例如,函数参数的求值顺序是未指定的,所以如果它们有副作用,这些副作用可以以任何顺序发生。这可能会打印“Hi!Ho!”或“嗨!嗨!”:

f( printf("Hi!"), printf("Ho!") );

这也可能因执行而异。正如标准所说: “因此,对于给定程序和给定输入,抽象机的一个实例可以具有多个可能的执行顺序。”不同之处在于 undefined 行为,任何事情都可能发生:计算机可能会爆炸、重新格式化磁盘或其他任何事情。如果行为未指定,则不允许计算机爆炸。

还有实现定义的行为,比如sizeof(int)的值。对于同一个编译器,这必须始终相同。

【讨论】:

这个解释简洁、合理,遵循“展示,不讲”的原则。未指明的行为呢?【参考方案3】:

未指定和未定义的行为不能保证在已编译程序的单独运行之间保持一致。仅这一点就已经使单独的编译之间的一致性概念完全没有意义。

此外,可能值得添加的是,未定义的行为可以通过阻止程序编译在编译阶段表​​现出来。

【讨论】:

【参考方案4】:

但是这种行为在 编译相同的程序 具有相同设置的相同编译器 一样的环境?

是的。

换句话说,如果我取消引用 文件 X.cpp 中第 78 行的空指针 和实现格式 在这种情况下开车是否意味着 它会在程序结束后做同样的事情 重新编译了吗?

未定义行为的结果几乎总是由编译器发出的代码以语言设计者未指定的方式与操作系统和/或硬件交互引起的。因此,如果您取消引用 NULL 指针,发生的事情实际上与编译器无关,而是取决于底层操作系统/硬件如何处理无效的内存访问。如果操作系统/硬件总是以一致的方式处理这个问题(例如通过陷阱),那么您可以期望 UB 是一致的,但这与语言或编译器无关。

【讨论】:

【参考方案5】:

我不知道未指明的行为(但从名称来看,也许它在任何地方都做同样的坏/坏事,只是没有人真正知道它到底做了什么)。但是对于未定义的行为,我认为这个行为在平台或编译器之间可能会有很大的不同。我在 Solaris 上看到了一些非常奇怪的核心转储,但在 Ubuntu 等上没有发生。

【讨论】:

我问的是在同一系统上使用相同的编译器和相同的设置(所有设置)重新编译。 抱歉,错过了。无论如何,我相信你不能(或至少不应该)依赖它。它只是未定义/未指定,这意味着几乎任何事情都可能发生。【参考方案6】:

这就是将其指定为未定义的目的......这意味着不知道会发生什么,无论是在不同的平台上,还是在相同的平台上(重复测试)。

【讨论】:

【参考方案7】:

值得注意的是,即使在今天,C++ 标准的指定行为的实现在不同的编译器中也不是 100% 相同的。鉴于此,期望未指定或未定义的行为与编译器无关是不合理的。如果你只遵守标准,你就有最好的机会编写可移植的代码。

【讨论】:

我不是在问编译器不可知论。在我的问题中,编译器每次都是一样的,设置是一样的,C++代码是一样的。 在这种情况下,是的,通常相同的输入会产生相同的输出。我见过一些案例,当我们更改看似不相关的一段代码或编译器设置时,我们依赖代码中的某些副作用惊人地失败了。 (IIRC,这些涉及非 100% 支持的效果模板实例化和严格的别名。)【参考方案8】:

当使用不同的优化级别或使用或不使用调试模式进行编译时,许多此类行为的实现方式不同。

【讨论】:

【参考方案9】:

不,这是标准中存在未定义/实现定义行为的部分原因。不保证在同一台计算机上多次编译同一源代码(例如,使用不同的优化标志)时未定义的行为是相同的。

委员会显然更喜欢定义明确的行为。当委员会认为某个概念存在多个实现时,实现定义的行为就存在,并且没有理由在所有情况下都偏爱一个。当委员会认为在合理的实现下难以兑现任何承诺时,就会出现未定义的行为。

在许多情况下,未定义的行为被实现为没有检查的东西。然后,该行为取决于操作系统,如果有的话,并且它是否注意到发生了不符合 kosher 的事情。

例如,取消引用不属于您的内存是未定义的。一般来说,如果你这样做,操作系统会杀死你的程序。但是,如果星星排列得恰到好处,您可以设法取消引用您在 C++ 规则下不拥有的内存(例如,您不是从 new 获得的,或者您已经从 deleted 获得它)但是操作系统相信你拥有。有时你会崩溃,有时你会破坏程序中其他地方的内存,有时你会在未被发现的情况下逃脱(例如,如果内存没有被归还)。

竞态条件被认为是未定义的,它们因在程序的不同运行期间不同而臭名昭著。如果您的操作系统没有注意到,每次您破坏堆栈时,您可能会得到不同的行为。

deletes 未定义。通常它们会导致崩溃,但它们未定义的事实意味着您不能依赖崩溃的东西。

【讨论】:

以上是关于是否需要在同一环境中使用同一编译器对同一程序进行编译之间保持一致的未指定和未定义行为?的主要内容,如果未能解决你的问题,请参考以下文章

是否允许 C++ 编译器发出编译同一程序的不同机器代码?

是否允许 C++ 编译器发出编译同一程序的不同机器代码?

zend 守卫解码(不是同一个问题)

对同一文件的不同部分使用不同的 C++ 标准

JVM的重排序

在同一个应用上使用两个 Facebook 应用 ID 进行测试