在常量表达式中除以零

Dividing by zero in a constant expression

本文关键字:常量 表达式      更新时间:2023-10-16

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

int x = 1 / 0;

C和/或c++标准允许这种行为吗?

是的,除零是未定义的行为,在这种情况下,C和c++标准都没有强加任何要求。尽管在这种情况下,我认为您至少应该发布一个诊断(参见下面的)。

在我引用标准之前,我应该注意到,尽管这可能是一致的行为实现的质量是一个不同的问题,仅仅是一致并不等同于有用。据我所知,gcc、clang、Visual Studio和Intel(tpg2114中的)团队认为内部编译器错误(ICEs)是应该报告的bug。应该注意的是,当前的gcc和clang都会对这种情况产生警告,而不管所提供的标志是什么。在两个操作数都是字面量/常量的情况下,就像我们这里的情况一样,检测并提供诊断似乎相当直接。Clang为这种情况生成以下诊断(查看实时):
warning: division by zero is undefined [-Wdivision-by-zero]
int x = 1 / 0 ;
^ ~

来自C11标准草案6.5.5乘法运算符(强调mine):

/操作符的结果是第一个操作数除以第二个;[…的值第二个操作数为零,则行为未定义。

,所以这是未定义的行为。

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 case

如果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 ;
^ ~

注意除以0是没有定义的

c++标准草案5.19Constant expressions [expr。const]说:

条件表达式e是核心常量表达式,除非e的求值符合抽象机器(1.9),将计算下列表达式之一

,并包括以下项目符号:

一个会有未定义行为的操作[注:包括,例如,有符号整数溢出](第5条),某些指针算术(5.7),除零(5.6),或某些移位操作(5.8)。-end note];

1/0是C11

中的常数表达式吗?1 / 0在C11中不是常量表达式,我们可以从6.6常量表达式中看到:

每个常量表达式的求值必须为可表示范围内的常量

虽然,它允许:

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

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

仅仅存在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在整型可表示的值范围内不是一个常数,因此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,那将是无稽之谈。

摘自C标准草案(N1570):

6.5.5乘法运算符…

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

以及第三章的未定义行为。术语、定义和符号:

3.4.3

  1. 未定义行为
    使用不可移植的或错误的程序结构或错误的数据时的行为;
  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/0求值为0。表达式求值函数在不能求值时将这些操作符的右操作数(以及其他操作符)移除,因此1 || 1/0转换为1 != 0(回想一下&&和||与0比较,结果为1,0 && 1/0转换为0 != 0,结果为0。

另一个需要注意的情况是INT_MIN / -1INT_MIN % -1(对于更大的整数类型也是如此)。商不能表示为有符号整型(在2的补数有符号整数的情况下,这是我们在所有现代cpu中所拥有的),所以这也是UB(您在运行时在x86上得到相同的除零异常)。我用同样的方法处理这种情况。该表达式不能初始化静态存储变量,如果没有在逻辑&&/||运算符中求值,则会被丢弃。它可以初始化一个自动变量,可能在运行时导致UB。

当遇到这样的除法时,我也会发出警告。

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

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

所以剩下的空格是编译器应该拒绝编译它,和