7

是否需要 C 实现来忽略在评估 void 表达式期间发生的未定义行为,就好像评估本身从未发生过一样?

考虑到 C11,6.3.2.2 §1:

如果任何其他类型的表达式被评估为 void 表达式,则其值或指示符将被丢弃。(评估 void 表达式的副作用。)

这与用于防止编译器警告未使用变量的常用习语有关:

void f() {
  int a;
  (void)a;
}

但是,如果我们有未定义的行为,例如:

void f() {
  int a;
  (void)(1/0);
}

我可以安全地声称该程序不包含未定义的行为吗?该标准说“它的值或指示符被丢弃”,但“表达式(...)被评估(...)”,所以评估似乎发生了。

GCC/Clang 确实报告了未定义的行为,因为在这种情况下很明显,但在一个更微妙的例子中它们没有:

int main() {
  int a = 1;
  int b = 0;
  (void)(a/b);
  return 0;
}

即使有-O0,GCC 和 Clang 都不会评估1/0。但是即使没有演员阵容也会发生这种情况,所以它不具有代表性。

将论点推向极端,(void)a在我的第一个示例(a未初始化的地方)中的简单评估不会系统地触发未定义的行为吗?

ISO C11 6.3.2.1 §2 确实提到:

如果左值指定了一个可以使用寄存器存储类声明的具有自动存储持续时间的对象(从未使用过它的地址),并且该对象未初始化(未使用初始化程序声明并且在使用之前未对其进行分配),行为未定义。

但是,在附件 J.2 未定义行为中,措辞略有不同:

在以下情况下,行为未定义:

(...)

一个左值指定一个可以用寄存器存储类声明的自动存储持续时间的对象,在需要指定对象的值的上下文中使用,但该对象未初始化。(6.3.2.1)。

这个附件确实导致了这样一种解释,即void在其评估期间包含未定义行为的表达式实际上并未被评估,但由于它只是一个附件,我不确定它的论证权重。

4

1 回答 1

5

这与用于防止编译器警告未使用变量的常用习语有关:

void f() {
  int a;
  (void)a;
}

是和不是。我认为这个习惯用法将一个未使用的变量变成了一个已使用的变量——它出现在一个表达式中——并使用强制转换void来防止编译器抱怨该表达式未使用的结果。但是在技术上,语言律师的意义上,这个习语的特定表达产生了 UB,因为当' 的值不确定时,子表达a会进行左值转换。a您已经引用了标准的相关文本。

但是,如果我们有未定义的行为,例如:

void f() {
  int a;
  (void)(1/0);
}

我可以安全地声称该程序不包含未定义的行为吗?

不。

该标准说“它的值或指示符被丢弃”,但“表达式(...)被评估(...)”,所以评估似乎发生了。

是的,就像a您之前示例中的表达式也被评估一样,也产生了 UB。UB 源于对内部子表达式的评估。转换为类型void是一个可分离的考虑因素,就像转换为任何其他类型一样。

GCC/Clang 确实报告了未定义的行为,因为在这种情况下很明显,但在一个更微妙的例子中它们没有:

编译器行为在这里不能被视为指示性的。C 不需要编译器来诊断大多数未定义的行为,即使是那些原则上可以在编译时检测到的行为。确实,重要的是要认识到由错误代码引起的 UB 首先发生在编译时,当然,如果生成了可执行文件,那么它也会出现 UB。

将论点推向极端, (void)a在我的第一个示例(a未初始化的地方)中的简单评估不会系统地触发未定义的行为吗?

是的,正如我已经说过的。但这并不意味着包含此类结构的程序有义务行为不端。作为实现质量问题,我认为希望表达式语句(void)a;被编译器接受并且根本没有相应的运行时行为是合理的。但我不能依靠语言标准来支持我。

这个附件确实导致了这样一种解释,即void在其评估期间包含未定义行为的表达式实际上并未被评估,但由于它只是一个附件,我不确定它的论证权重。

标准规范文本的简单措辞在这里就足够了。附件不是规范性的,但如果对如何解释规范性文本有任何疑问,那么标准的信息性部分,如附件 J,是整理时考虑的来源之一(但它们仍然只是提供信息)。

于 2019-07-24T12:42:39.923 回答