【问题标题】:Can code that will never be executed invoke undefined behavior?永远不会执行的代码可以调用未定义的行为吗?
【发布时间】:2013-08-25 10:53:30
【问题描述】:

调用未定义行为(在本例中,除以零)的代码将永远不会被执行,程序仍然是未定义行为吗?

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

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

那么,有什么想法吗?

【问题讨论】:

  • 如果它从未被执行,我会说它不是“行为”
  • 如果 UB 是运行时之一(像这样) - 它不会。但我非常怀疑标准对此有何看法。
  • 听起来像是语义问题,而不是编程问题。
  • @Wooble 我不同意。短语未定义的行为 在 C/C++ 中具有特殊含义。这个问题与其他一些决定未定义行为的情况有关。作为记录,如果您阅读过 C/C++ 标准,您会发现到处都是短语未定义的行为
  • @Cornstalks:C 标准不使用“调用未定义的行为”这个短语,所以你不能根据这个短语的含义来推断 C 标准。用它来描述 C 是不恰当的,因为它暗示“未定义的行为”是一种事物,例如如果你越界就会碰到一堵墙。实际上,“未定义的行为”是一种缺乏。这是界限的终结。当您离开标准 C 的定义明确的城镇时,您将进入一个可以建造任何东西的开阔场地。

标签: c language-lawyer


【解决方案1】:

让我们看看 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 不是常量表达式

constant-expression 是一个简化为 conditional-expression 的句法类别(不包括赋值和逗号表达式)。产生式constant-expression 出现在语法only 中实际需要常量表达式的上下文中,例如case 标签。所以如果你写:

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

然后1/0 是一个常量表达式——并且违反了 6.6p4 中的约束:“每个常量表达式都应计算为可表示范围内的常量 其类型的值。”,因此需要诊断。但赋值的右侧不需要 constant-expression,只需要 conditional-expression,所以常量表达式的约束不适用。编译器可以评估它在编译时能够评估的任何表达式,但前提是行为与在执行期间评估的行为相同(或者,在if (0) 的上下文中, 在执行期间评估()。

(看起来完全像 constant-expression 的东西不一定是 constant-expression,就像在 x + y * z 中,序列 x + y 不是additive-expression 因为它出现的上下文。)

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

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

实际上与这个问题无关。

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

  • 非空源文件不以换行符结尾,该换行符之前不紧跟反斜杠字符或以部分结尾 预处理令牌或评论
  • 标记连接产生与通用字符名称语法匹配的字符序列
  • 在源文件中遇到不在基本源字符集中的字符,标识符、字符常量、字符串除外 字面量、标题名称、注释或预处理标记 从未转换为令牌
  • 标识符、注释、字符串文字、字符常量或标头名称包含无效的多字节字符或没有开始 并以初始移位状态结束
  • 同一个标识符在同一个翻译单元中既有内部链接又有外部链接

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

【讨论】:

  • @EricPostpischil 6.6/4 说“每个常量表达式都应计算为一个常量,该常量在其类型的可表示值范围内。”那不是将1/0 排除在常量表达式之外吗?
  • @EricPostpischil:我认为这不太对。违反约束通常意味着需要编译时诊断,而不仅仅是可能是 foo 的东西不是 foo1/0 不是在问题的上下文中 的常量表达式,因为它没有被解析为constant-expression,而仅仅是conditional-expression 这是 assignment-expression 的一部分。 case 1/0: 会违反约束并需要诊断。
  • DR#109 好像表明程序不是UB,看我刚刚发布的新答案。
  • Re "so we fall back to the common English meaning",在英文意思中,如果程序中存在结构,则程序使用它。那么为什么您的答案假设使用构造意味着执行构造?您的解释与您的结论不符!
【解决方案2】:

article 在第 2.6 节讨论了这个问题:

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

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

标准11 背后的意图似乎是,通常情况下,如果不容易为它们生成代码,它们就会静态地未定义。只有当代码可以生成时,情况才能动态定义。

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

我建议您查看整篇文章。总之,它描绘了一幅一致的画面。

文章作者必须与委员会成员讨论该问题这一事实证实,该标准目前对您的问题的答案是模糊的。

【讨论】:

  • 该示例提出的困难在于您是否可以静态确定其行为是否未定义。当它运行时(假设guard() 的行为未定义),当且仅当语句5 / 0; 实际执行时,行为才是未定义的。 (请注意,编译器可以合法地将 5 / 0 的评估替换为对 abort() 的调用或类似的东西;当且仅当执行到达该点时,程序才会中止。)编译器可能拒绝 该程序只有在它可以确定guard() 将始终终止时。
  • @KeithThompson 为了澄清文章中的静态/动态区别,5/0 被认为是动态,因为编译器可以生成除以零的代码:只需生成通常的代码在将 z 设置为 0 后除以 z。因此,天真的编译器可以生成除法指令。确定guard() 不会终止的复杂编译器根本不需要为 5/0 生成任何代码。相反,没有办法为(int)(void)5 生成代码,不能只为(int)(void)z 生成代码,因为那也不正确。所以作者认为……
  • @KeithThompson ... 允许编译器拒绝程序 if (0) (int)(void)5;,因为它给幼稚的编译器带来了难题,而无法访问的动态 UB,例如 if (0) 5 / 0; 是无害的。这是他们与委员会成员讨论的结果,我在其他地方看到了类似的论点(但可能来自同一来源,特别是因为我不记得它在哪里)。我目前正在研究 C99 的基本原理,如果我看到任何提及这一点,我会回来指出。
  • (int)(void)5 是违反约束的。 N1570 6.5.4,描述强制转换运算符:“约束:除非类型名称指定 void 类型,否则类型名称应指定原子、合格或不合格的标量类型,并且操作数应具有标量类型。”。 (void)5 没有标量类型,因此 (int)(void)5 违反了该约束,无论包含它的代码是否曾经执行过。
  • @KeithThompson 是的,他们似乎选择了错误的例子,但是在 J.2 的长列表中,有一个不是违反约束的,而且是“静态的”,确定吗?那句老话,“非空源文件不以换行符结尾……”怎么样?没有适用于这个的可达性概念,但它不是违反约束的,不是吗?
【解决方案3】:

在这种情况下,未定义的行为是执行代码的结果。所以如果代码没有被执行,就没有未定义的行为。

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

【讨论】:

  • 以案例 #2 为例,考虑调用 UB 的 #include "//e"
【解决方案4】:

我会选择这个答案的最后一段:https://stackoverflow.com/a/18384176/694576

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

所以,不,没有调用 UB。

【讨论】:

  • 您不应该相信您在互联网上阅读的所有内容,尤其是来自 StackOverflow 的答案。
  • @PascalCuoq 这打破了像我这样的几个 SO 信徒的信仰。现在去哪里?
【解决方案5】:

仅当标准做出重大更改并且您的代码突然不再“永远不会执行”时。但我看不出有任何合乎逻辑的方式会导致“未定义的行为”。它不会导致任何事情

【讨论】:

    【解决方案6】:

    关于未定义行为的主题,通常很难将形式方面与实际方面分开。这是 1989 年标准中未定义行为的定义(我手头没有更新的版本,但我预计这不会发生重大变化):

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

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

    另一方面,从实际的角度来看,我会惊讶地发现编译器的行为与您的直觉预期不同。

    【讨论】:

      【解决方案7】:

      标准说,我没记错,从那一刻起就可以做任何事情,规则被打破。也许有一些具有全球风味的特殊事件(但我从未听说过或读过类似的东西)......所以我会说:不,这不可能是 UB,因为只要行为被明确定义,0 总是false,因此规则不会在运行时被破坏。

      【讨论】:

      • 0 总是正确的?甚至总是正确的?你是红宝石爱好者吗?!
      • @Grady Player 不,我只是喜欢某种大脑。我会修复它,对不起
      【解决方案8】:

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

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

      Defect Report #109 解决了类似的问题并说:

      此外,如果给定程序的每一次可能的执行都会导致未定义的行为,那么给定的程序就不是严格符合的。 一个符合的实现不能仅仅因为该程序的某些可能的执行会导致未定义的行为而不能翻译一个严格符合的程序。因为 foo 可能永远不会被调用,所以给出的示例必须由符合要求的实现成功翻译。

      【讨论】:

        【解决方案9】:

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

        这个程序看起来像 C,因此对编译器使用的 C 标准(正如一些答案所做的那样)进行更深入的分析是合适的。

        在没有指定标准的情况下,正确答案是“视情况而定”。根据编译器的猜测,在某些语言中,编译器在出现第一个错误后会尝试猜测程序员的意思,但仍会生成一些代码。在其他更纯粹的语言中,一旦某些东西未定义,未定义就会传播到整个程序。

        其他语言有“有界错误”的概念。对于某些有限类型的错误,这些语言定义了错误可能产生的破坏程度。在具有隐含垃圾收集的特定语言中,错误是否会使输入系统无效通常会产生影响。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-01-02
          • 2018-07-31
          • 1970-01-01
          • 2020-12-07
          • 1970-01-01
          • 2014-08-04
          相关资源
          最近更新 更多