43

如果我在常量表达式中除以零,我的玩具编译器会崩溃:

int x = 1 / 0;

C 和/或 C++ 标准是否允许这种行为?

4

5 回答 5

40

是的,除以零是未定义的行为,在这种情况下,C 和 C++ 标准都没有强加任何要求。尽管在这种情况下,我相信您至少应该发出诊断信息(见下文)。

在我引用标准之前,我应该注意,虽然这可能是符合行为的实施质量是一个不同的问题,但仅仅符合并不等于有用。据我所知,gcc、clang、Visual Studio 和 Intel(根据 tpg2114)团队认为内部编译器错误(ICE)是应该报告的错误。应该注意的是,当前的 gcc 和 clang 都会针对这种情况产生警告,似乎与提供的标志无关。在两个操作数都是文字/常量的情况下,我们在这里遇到的情况,检测并为此提供诊断似乎相当简单。clang 为这种情况生成以下诊断信息(现场查看):

warning: division by zero is undefined [-Wdivision-by-zero]
int x = 1 / 0 ;
          ^ ~

从草案 C11 标准部分6.5.5乘法运算符(强调我的):

/ 运算符的结果是第一个操作数除以第二个操作数的商;[...]如果第二个操作数的值为零,则行为未定义。

所以这是未定义的行为。

C++ 标准部分草案5.6 [expr.mul]说:

二元 / 运算符产生商 [...]如果 / 或 % 的第二个操作数为零,则行为未定义[...]

再次未定义的行为。

C++ 标准草案和 C 标准草案对未定义行为都有类似的定义,两者都说:

[...] 本国际标准对此没有任何要求

这句话没有任何要求似乎太允许任何行为,包括鼻恶魔。两者都有类似的说明,大致如下:

当本国际标准省略任何明确的行为定义或程序使用错误的构造或错误的数据时,可能会出现未定义的行为。允许的未定义行为的范围从完全忽略具有不可预测结果的情况,到在翻译或程序执行期间以环境特征的记录方式表现(有或没有发出诊断消息),到终止翻译或执行(有发出的诊断消息)

因此,虽然注释不规范,但似乎如果您要在翻译过程中终止,您至少应该发出诊断。术语终止没有定义,因此很难争论这允许什么。我认为我没有见过 clang 和 gcc 在没有诊断的情况下使用 ICE 的情况。

代码必须执行吗?

如果我们阅读永远不会执行的代码可以调用未定义的行为吗?我们可以看到,至少在 C 的情况下,有争论的余地,1 / 0必须执行才能调用未定义的行为。在 C++ 案例中更糟糕的是,不存在行为定义,因此用于 C 案例的部分分析不能用于 C++ 案例。

似乎如果编译器可以证明代码永远不会被执行,那么我们可以推断它好像程序没有未定义的行为,但我不认为这是可证明的,只是合理的行为。

从 C 的角度来看,WG14 缺陷报告 109进一步阐明了这一点。给出以下代码示例:

int foo()
{
  int i;
  i = (p1 > p2); /* Must this be "successfully translated"? */
  1/0; /* Must this be "successfully translated"? */
  return 0;
} 

回应包括:

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

因此,在 C 的情况下,除非可以保证调用未定义行为的代码将被执行,否则编译器必须成功翻译程序。

C++ constexpr 案例

如果x是 constexpr 变量:

constexpr int x = 1 / 0 ;

它将是格式错误的,并且 gcc 会产生警告,而 clang 会使其出错(实时查看):

error: constexpr variable 'x' must be initialized by a constant expression
constexpr int x = 1/ 0 ;
             ^   ~~~~
note: division by zero
constexpr int x = 1/ 0 ;
                  ^
warning: division by zero is undefined [-Wdivision-by-zero]
constexpr int x = 1/ 0 ;
                  ^ ~

有助于注意除以零是未定义的

草案 C++ 标准部分5.19常量表达式 [expr.const] 说:

条件表达式 e 是核心常量表达式,除非按照抽象机 (1.9) 的规则对 e 的求值将求值以下表达式之一

并包括以下项目符号:

具有未定义行为的操作[注意:包括,例如,有符号整数溢出(第 5 条)、某些指针算术(5.7)、除以零(5.6)或某些移位操作(5.8)——尾注];

1 / 0 是 C11 中的常量表达式吗

1 / 0不是 C11 中的常量表达式,我们可以从6.6常量表达式部分看到这一点,它说:

每个常量表达式都应计算为一个常量,该常量在其类型的可表示值范围内。

虽然,它确实允许:

一个实现可以接受其他形式的常量表达式。

所以1 / 0不是 C 或 C++ 中的常量表达式,但这不会改变答案,因为它没有在需要常量表达式的上下文中使用。我怀疑 OP 意味着它1 / 0可用于常量折叠,因为两个操作数都是文字,这也可以解释崩溃。

于 2015-11-25T12:09:38.913 回答
24

仅仅存在1 / 0不允许编译器崩溃。最多允许假设表达式永远不会被计算,因此,执行永远不会到达给定的行。

如果保证对表达式求值,则标准对程序或编译器没有要求。然后编译器可能会崩溃。

1 / 0 仅在评估时为 UB。

C11 标准给出了一个在未评估时被定义行为的明确示例:1 / 0

因此,在下面的初始化中,

        static int i = 2 || 1 / 0;

该表达式是一个有效的整数常量表达式,值为 1。

第 6.6 节,脚注 118。

1 / 0 不是常量表达式。

C11 标准的第 6.6 节,在约束下,说

  1. 常量表达式不应包含赋值、递增、递减、函数调用或逗号运算符,除非它们包含在未计算的子表达式中。
  2. 每个常量表达式都应计算为一个常量,该常量在其类型的可表示值范围内。

由于 1/0 不会计算为 int 可表示的值范围内的常量,因此 1/0 不是常量表达式。这是关于什么算作常量表达式的规则,就像关于其中没有赋值的规则一样。您可以看到,至少对于 C++,Clang 不认为 1/0 是常量表达式

prog.cc:3:18: error: constexpr variable 'x' must be initialized by a constant expression
   constexpr int x = 1/ 0 ;
                 ^   ~~~~

未评估的 1 / 0 是 UB 没有多大意义。

(x == 0) ? x : 1 / x是完美定义的,即使 x 是 0 并且评估 1/x 是 UB。如果(0 == 0) ? 0 : 1 / 0是UB的情况,那将是无稽之谈。

于 2015-11-25T17:49:22.493 回答
15

来自 C 标准草案 (N1570):

6.5.5 乘法运算符

...

  1. / 运算符的结果是第一个操作数除以第二个操作数的商;% 运算符的结果是余数。在这两种操作中,如果第二个操作数的值为零,则行为未定义。

关于第 3 章中未定义的行为。术语、定义和符号:

3.4.3

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

因此允许编译器崩溃。

于 2015-11-25T12:09:15.833 回答
2

其他人已经提到了标准中的相关文字,所以我不再赘述。

我的 C 编译器的表达式评估函数采用逆波兰表示法(值数组(数字和标识符)和运算符)的表达式并返回两件事:表达式是否计算为常量的标志以及如果它是常量的值 ( 0 否则)。如果结果是一个常数,则整个 RPN 会减少到该常数。1/0 不是常量表达式,因为它不会计算为常量整数值。RPN 不会减少 1/0 并保持不变。

在 C 中,静态变量只能用常量值初始化。因此,当编译器发现静态变量的初始值设定项不是常量时,编译器会出错。自动存储的变量可以用非常数表达式初始化。在这种情况下,我的编译器生成代码来评估 1/0(它仍然有这个表达式的 RPN!)。如果在运行时达到此代码,则按照语言标准的规定发生 UB。[在 x86 上,此 UB 采用除以零 CPU 异常的形式,而在 MIPS 上,此 UB 产生不正确的商值(CPU 没有除以零异常)。]

我的编译器正确地支持 ||-expressions 和 &&-expressions 中的短路。因此,它的计算结果1 || 1/0为 1 和0 && 1/00,无论逻辑运算符的右手操作数是否为常数。表达式求值函数在这些运算符(连同运算符)不能被求值时删除它们的右手操作数,因此1 || 1/0转换为1 != 0(回想一下 && 和 || 的操作数与 0 进行比较),产生 1 并0 && 1/0转换成0 != 0,产生 0。

另一种需要注意的情况是INT_MIN / -1and INT_MIN % -1(对于较大的整数类型也是如此)。商不能表示为有符号整数(在 2 的补码有符号整数的情况下,这是我们在所有现代 CPU 中都有的),所以这也是 UB(在 x86 运行时,您会得到相同的除以零异常) . 我同样处理这个案子。此表达式无法初始化静态存储变量,如果未在逻辑 &&/|| 中求值,则将其丢弃 操作员。它可以初始化一个自动变量,可能在运行时导致 UB。

当遇到这种划分时,我也会发出警告。

于 2015-12-02T00:32:37.633 回答
-1

编译器的行为方式与表达式的值无关。编译器不应该崩溃。时期。

我想一个迂腐的实现,给定这样的表达式,将编译为将在运行时执行 1/0 的代码,但我认为这不会被视为一个好的特性。

所以剩下的空间是编译器应该拒绝编译它,并将其视为某种源代码错误。

于 2015-12-01T22:39:34.313 回答