永远不会执行的代码可以调用未定义的行为吗?


81

调用未定义行为(在此示例中为零)的代码将永远不会执行,程序是否仍是未定义行为?

int main(void)
{
    int i;
    if(0)
    {
        i = 1/0;
    }
    return 0;
}

我认为这仍然是不确定的行为,但是我在标准中找不到任何证据来支持或否认我。

那么,有什么想法吗?


7
我要说的是,即使从未执行,也不是“行为”
凯文

1
如果UB是运行时之一(像这样),则不会。但是我非常怀疑标准对此有何评论。
凯塔尔

13
听起来像语义问题,而不是编程问题。
Wooble

14
@Wooble我不同意。短语未定义行为在C / C ++中具有特殊含义。这个问题与确定是否未定义行为的其他情况有关。作为记录,如果您已经阅读了C / C ++标准,那么到处都会发现短语undefined behavior
于昊

10
@Cornstalks:C标准未使用短语“调用未定义的行为”,因此您无法根据此短语的含义来推论C标准。用它来描述C是不合适的,因为它表明“未定义行为”是诸如越界就会碰到的墙之类的东西。实际上,“不确定的行为”是一件事。这是界限的尽头。当您离开定义良好的标准C镇时,您将处于一个可以建造任何东西的开放领域。
Eric Postpischil 2013年

Answers:


70

让我们看看C标准如何定义术语“行为”和“未定义行为”。

参考的是ISO C 2011标准的N1570草案;我不知道三个已发布的ISO C标准(1990、1999和2011)中的任何相关差异。

第3.4节:

行为
外观或动作

好的,这有点含糊,但是我认为给定的语句没有“外观”,当然也没有“动作”,除非它是实际执行的。

第3.4.3节:


使用本国际标准不要求的非便携式或错误程序构造或错误数据时的不确定行为

它说这样的构造“在使用时”。该标准未定义“使用”一词,因此我们回到常见的英语含义。如果从未执行过构造,则不会“使用”。

在该定义下有一个注释:

注意可能的不确定行为包括从完全忽略情况以无法预测的结果到在翻译或程序执行过程中以环境特有的文件化方式表现的行为(有或没有发出诊断消息)到终止翻译或执行(有发出诊断消息)。

因此如果未定义行为,则允许编译器在编译时拒绝程序。但是我对此的解释是,只有证明程序的每次执行都会遇到未定义的行为,它才能这样做。我认为这意味着:

if (rand() % 2 == 0) {
    i = i / 0;
}

当然可以具有不确定的行为,不能在编译时被拒绝。

实际上,程序必须能够执行运行时测试,以防止调用未定义的行为,并且标准必须允许它们这样做。

您的示例是:

if (0) {
    i = 1/0;
}

永远不会执行除以0的操作。一个非常常见的习惯用法是:

int x, y;
/* set values for x and y */
if (y != 0) {
    x = x / y;
}

如果y == 0,该部门当然具有未定义的行为,但如果,则永远不会执行y == 0。行为定义得很好,并且出于与示例定义得很好相同的原因:因为潜在的未定义行为永远不会发生。

(除非INT_MIN < -INT_MAX && x == INT_MIN && y == -1(是的,整数除法可能会溢出),但这是一个单独的问题。)

在注释中(因为删除),有人指出编译器可以在编译时评估常量表达式。正确,但在这种情况下不相关,因为在

i = 1/0;

1/0 不是常量表达式

常数表达式是一个句法类别减少到条件表达式(其不包括分配和逗号表达式)。产生常量表达式在实际需要常量表达式(例如大小写标签)的上下文中出现在语法中。因此,如果您写:

switch (...) {
    case 1/0:
    ...
}

然后1/0是一个常量表达式-并且违反了6.6p4中的约束:“每个常量表达式的求值结果必须是其类型的可表示值范围内的常量。”,因此需要进行诊断。但是赋值的右边不需要常量表达式,只需要条件表达式,因此对常量表达式的约束不适用。编译器可以评估在编译时能够执行的任何表达式,但前提是该行为与在执行过程中进行了评估(或在上下文中if (0),在execute()期间进行评估)相同。

(看上去完全像常量表达式的东西不一定是常量表达式,就像在中x + y * z,由于序列的上下文不同,该序列x + y也不是加性表达式。)

这意味着我要引用的N1570第6.6节中的脚注:

因此,在下面的初始化中,
static int i = 2 || 1 / 0;
该表达式是值为1的有效整数常量表达式。

实际上与这个问题无关。

最后,定义了一些导致未定义行为的东西,这些东西与执行期间发生的事情无关。C标准的附件J,第2节(再次参见N1570草案)列出了导致未定义行为的内容,这些内容是从该标准的其余部分收集来的。一些示例(我不认为这是详尽的列表)是:

  • 非空源文件不会以换行符结尾,而该换行符不能紧跟反斜杠字符,也不能以部分预处理标记或注释结尾
  • 令牌串联产生与通用字符名称的语法匹配的字符序列
  • 源文件中会遇到基本源字符集中没有的字符,但标识符,字符常量,字符串文字,标头名称,注释或永远不会转换为令牌的预处理令牌除外
  • 标识符,注释,字符串文字,字符常量或标题名称包含无效的多字节字符,或者不以初始移位状态开头和结尾
  • 相同的标识符在同一翻译单元中具有内部和外部链接

这些特殊情况是编译器可以检测到的。我认为他们的行为是不确定的,因为委员会不想或不可能将相同的行为强加给所有实现,并且定义一系列允许的行为是不值得的。它们并不是真正属于“永远不会执行的代码”的类别,但是出于完整性的考虑,我在这里提到它们。


2
@EricPostpischil 6.6 / 4说:“每个常量表达式的求值结果必须为该类型的可表示值范围内的常量。” 难道不排除1/0要成为常量表达式吗?
Casey

2
@EricPostpischil:我认为那是不对的。违反约束通常意味着需要进行编译时诊断,而不仅仅是使原本可能是foo的东西不是foo在问题的上下文中1/0不是常量表达式因为它没有被解析为常量表达式,而只是被解析为赋值表达式的一部分的条件表达式case 1/0:将违反约束条件并需要诊断。
基思·汤普森

DR#109似乎表明该程序不是UB,请参阅我刚刚发布的新答案。
哇,2016年

1
关于“,所以我们退回到通用的英语含义”,在英语含义中,如果程序中存在该程序,则使用该结构。那么,为什么您的答案假定使用构造意味着执行构造?您的结论并非来自您的解释!
ikegami

31

文章讨论了第2.6节这样一个问题:

int main(void){
      guard();
      5 / 0;
}

作者认为该程序是在guard()不终止时定义的。他们还发现自己区分“静态未定义”和“动态未定义”的概念,例如:

标准11的意图似乎是,通常情况下,如果不容易为情境生成代码,就使情境成为静态未定义的。仅当可以生成代码时,情况才能动态取消定义。

11)与委员会成员的私人通信。

我建议看整篇文章。综上所述,它描绘了一幅一致的图画。

文章的作者必须与委员会成员讨论该问题的事实证实,该标准目前对您的问题的答案尚不明确。


1
该示例在您是否可以静态确定其行为是否未定义方面带来困难。当它运行时(假设of的行为guard()未定义),只有且仅当该语句5 / 0;实际执行时,行为才是未定义的。(请注意,编译器可以用对之5 / 0的调用abort()或类似的方法合法地代替对的求值;然后且仅当执行到达该点时,程序才会中止。)只有当编译器确定可以永远终止时,程序才可以拒绝该程序guard()
基思·汤普森

@KeithThompson为了澄清文章中的静态/动态区别,将5/0视为动态的,因为编译器可以生成除以零的代码:只需将z设置为0后生成通常的除以z的代码。因此,天真的编译器可以生成除法指令。确定guard()不终止的复杂编译器根本不必为5/0生成任何代码。相反,没有办法为生成代码(int)(void)5,也不能仅为生成代码(int)(void)z,因为这也不正确。所以作者认为…
Pascal Cuoq

@KeithThompson…允许编译器拒绝程序,if (0) (int)(void)5;因为它给幼稚的编译器带来了难题,而无法访问的动态UBif (0) 5 / 0;则是无害的。这是从他们与委员会成员的讨论中得到的结果,我在其他地方也看到过类似的论点(但也许来自同一来源,特别是因为我不记得它在哪里)。目前,我正在研究C99的基本原理,如果有任何提及,我会回来指出。
Pascal Cuoq

2
(int)(void)5是违反约束的。N1570 6.5.4,描述了强制转换运算符:“约束:除非类型名称指定了void类型,否则类型名称应指定原子,合格或不合格的标量类型,并且操作数应具有标量类型。”。(void)5没有标量类型,因此(int)(void)5违反了该约束,无论包含它的代码是否曾经执行过。
基思·汤普森

@KeithThompson是的,他们似乎选择了错误的示例,但是在J.2的长长列表中,肯定有一个不是约束违例且是“静态”的吗?那古老的经典“非空源文件没有以换行符结尾...”怎么样?没有适用于此的可到达性概念,但这不是违反约束,对吗?
Pascal Cuoq

5

在这种情况下,未定义的行为是执行代码的结果。因此,如果未执行代码,则不会有未定义的行为。

如果未执行的行为仅是代码声明的结果,则未执行的代码可能会调用未定义的行为(例如,如果变量影子的某些情况未定义)。


对于情况2的示例,请考虑#include "//e"哪个调用UB。
Michael Foukarakis

2

我会回答这个问题的最后一段:https : //stackoverflow.com/a/18384176/694576

... UB是运行时问题,而不是编译时问题...

因此,不,没有UB被调用。


2
您不应该相信您在互联网上阅读的所有内容,尤其是不要从StackOverflow答案中获取任何信息。
Pascal Cuoq

1
@PascalCuoq破坏了像我这样的几个SO信徒的信仰。现在要去哪里?
0decimal0

2

只有当标准进行重大更改并且您的代码突然不再“永远不会执行”时,才可以使用。但是我看不出有什么逻辑方法可以导致“未定义的行为”。它什么也没引起。


2

对于行为不确定的问题,通常很难将形式方面与实际方面分开。这是1989年标准中未定义行为的定义(我手头没有较新的版本,但我不希望这有很大的变化):

1个未定义的行为
  使用不可移植或错误的程序构造或
  错误数据,本国际标准对此不施加任何要求
2注意可能的不确定行为范围为完全忽略情况
  在翻译或程序执行过程中表现出不可预测的结果
  以书面形式记录环境特征(有或没有
  发出诊断消息),以终止翻译或
  执行(发出诊断消息)。

从正式的角度来看,我想说您的程序确实会调用未定义的行为,这意味着该标准对运行时将执行的操作没有任何要求,仅因为它包含零除。

另一方面,从实际的角度来看,我会惊讶地发现编译器的运行方式不符合您的直觉期望。


2

该标准说,正如我所记得的对,从一开始就违反规则就可以做任何事情。也许有一些具有全球风味的特殊事件(但我从未听说过或读过类似的东西)...所以我会说:不,这不可能是UB,因为只要行为已明确定义,始终为0 false,因此规则在运行时不会被破坏。


0永远是真的吗?甚至一直都是真的?你是某种红宝石主义者吗?
Grady Player

@Grady Player不,我是某种大脑afk。我要修复它,对不起
dhein

2

我认为这仍然是不确定的行为,但是我在标准中找不到任何证据来支持或否认我。

我认为该程序不会调用未定义的行为。

缺陷报告109解决了类似的问题,并说:

此外,如果给定程序的所有可能执行都将导致未定义的行为,则该给定程序不是严格符合要求的。合格的实现一定不能仅仅因为某个程序的某些可能执行会导致不确定的行为而翻译严格合格的程序。由于可能永远不会调用foo,因此必须通过符合标准的实现成功翻译给出的示例。


-1

它取决于如何定义表达式“未定义行为”,以及语句的“未定义行为”是否与程序的“未定义行为”相同。

该程序看起来像C,因此更深入地分析编译器使用的C标准(如某些答案一样)。

在没有指定标准的情况下,正确的答案是“取决于”。在某些语言中,根据编译器的猜测,在出现第一个错误之后,编译器会尝试猜测程序员可能意味着什么,并且仍会生成一些代码。在其他更纯净的语言中,一旦不确定某些内容,不确定性就会传播到整个程序。

其他语言具有“有界错误”的概念。对于某些有限类型的错误,这些语言定义了错误可能造成的损害。尤其是具有隐式垃圾回收功能的语言通常会因错误使输入系统无效而有所不同。

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.