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

Yu *_*Hao 79 c language-lawyer

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

int main(void)
{
    int i;
    if(0)
    {
        i = 1/0;
    }
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

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

那么,有什么想法吗?

Kei*_*son 70

让我们看一下C标准如何定义术语"行为"和"未定义的行为".

参考ISO 15 2011标准的N1570草案; 我不知道三个公布的ISO C标准(1990年,1999年和2011年)中的任何一个都存在任何相关差异.

第3.4节:

行为
外观或行为

好吧,这有点模糊,但我认为给定的陈述没有"外观",当然也没有"行动",除非它实际执行.

第3.4.3节:


使用的行为行为,在使用不可移植或错误的程序结构或错误数据时,本国际标准不对其施加任何要求

它说" 使用 "这种结构.标准没有定义"使用"这个词,所以我们回到了常用的英语含义.如果构造从未执行过,则不会"使用"构造.

根据该定义有一条说明:

注意可能的未定义行为包括完全忽略具有不可预测结果的情况,在转换或程序执行期间以环境特征(有或没有发出诊断消息)的特定文档执行,终止转换或执行(使用发布诊断消息).

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

if (rand() % 2 == 0) {
    i = i / 0;
}
Run Code Online (Sandbox Code Playgroud)

当然可以有未定义的行为,不能在编译时拒绝.

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

你的例子是:

if (0) {
    i = 1/0;
}
Run Code Online (Sandbox Code Playgroud)

从来没有用0来执行除法.一个非常常见的习语是:

int x, y;
/* set values for x and y */
if (y != 0) {
    x = x / y;
}
Run Code Online (Sandbox Code Playgroud)

如果分裂肯定有未定义的行为y == 0,但它永远不会被执行y == 0.行为已明确定义,并且出于同样的原因,您的示例已明确定义:因为潜在的未定义行为实际上永远不会发生.

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

在注释中(自删除​​后),有人指出编译器可能在编译时计算常量表达式.这是真的,但在这种情况下不相关,因为在上下文中

i = 1/0;
Run Code Online (Sandbox Code Playgroud)

1/0 不是一个恒定的表达.

常数表达式是一个句法类别减少到条件表达式(不包括分配和逗号表达式).生成常量表达式在实际需要常量表达式的上下文中出现在语法中,例如案例标签.所以,如果你写:

switch (...) {
    case 1/0:
    ...
}
Run Code Online (Sandbox Code Playgroud)

然后1/0是一个常量表达式 - 并且违反了6.6p4中的约束:"每个常量表达式应该计算为其类型的可表示值范围内的常量."因此需要进行诊断.但是赋值的右侧不需要常量表达式,只需要条件表达式,因此常量表达式的约束不适用.编译器可以评估它能够在编译时任何表情,但只有当行为是一样的,如果它在执行过程中评估(或在的情况下if (0),()执行过程中进行评估.

(看起来完全像常量表达式的东西不一定是常量表达式,就像x + y * z在序列x + y不是加法表达式一样,因为它出现的上下文.)

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

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

实际上与这个问题无关.

最后,有一些事情被定义为导致未定义的行为,而不是在执行期间发生的事情.附录J,C标准的第2部分(再次参见N1570草案)列出了从标准的其余部分收集的导致未定义行为的事物.一些例子(我不认为这是一个详尽的清单)是:

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

这些特殊情况是编译器可以检测到的.我认为他们的行为是未定义的,因为委员会不希望或不能在所有实施中强加相同的行为,并且定义一系列允许的行为是不值得的.它们并不属于"永远不会执行的代码"类别,但我在这里提到它们是为了完整性.

  • @EricPostpischil 6.6/4 说“每个常量表达式都应评估为在其类型的可表示值范围内的常量。” 这不会将 `1/0` 从常量表达式中排除吗? (2认同)
  • @EricPostpischil:我认为这不太对。违反约束通常意味着需要编译时诊断,而不仅仅是可能是 *foo* 的东西不是 *foo*。`1/0` 不是*在问题上下文中的常量表达式*,因为它没有被解析为*常量表达式*,而只是作为*赋值表达式*的一部分的*条件表达式*。`case 1/0:` 将违反约束并需要诊断。 (2认同)
  • 关于“*所以我们回到常见的英语含义*”,在英语含义中,程序使用一个结构,如果它存在于程序中。那么为什么你的答案假设使用构造意味着执行构造呢?你的结论不能从你的解释中得出! (2认同)

Pas*_*uoq 31

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

int main(void){
      guard();
      5 / 0;
}
Run Code Online (Sandbox Code Playgroud)

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

标准11背后的意图似乎是,一般情况下,如果不容易为它们生成代码,则静态地定义情况.只有在可以生成代码时,才能动态地定义情境.

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

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

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

  • `(int)(void)5`是一个约束违规.N1570 6.5.4,描述了强制转换操作符:"约束:除非类型名称指定了void类型,否则类型名称应指定原子,限定或不合格的标量类型,操作数应具有标量类型.".`(void)5`没有标量类型,因此`(int)(void)5`违反了该约束,无论是否执行包含它的代码. (2认同)

Arn*_*anc 5

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

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