在常量表达式中除以零

Posted

技术标签:

【中文标题】在常量表达式中除以零【英文标题】:Dividing by zero in a constant expression 【发布时间】:2016-02-28 05:16:24 【问题描述】:

如果我,我的玩具编译器会崩溃:

int x = 1 / 0;

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

【问题讨论】:

所以 x 不是 constexpr 变量,只是确保,否则 C++ 标准会说 If the second operand of / or % is zero the behavior is undefined 我相信你不会写一个玩具 C++ 编译器。任何符合 C++11 的编译器都不是玩具;但是你可以用 C++ 为你的玩具语言编写一个玩具编译器!然后你就定义了你的语言标准,C++ 标准并不重要! 我希望回答者发表意见的一个变体是是否允许if (0) int x = 1 / 0; 使编译器崩溃或生成崩溃的程序。 @PascalCuoq: ***.com/a/18385138/2003898 -> 不,不允许崩溃,它已经定义好了。 afaik 该标准没有说明编译器崩溃,而是编译程序崩溃。我真的不认为编译器在遇到例如崩溃时是不允许的。 int x = 1*0;,这样的编译器是否有用是另一个问题:P 【参考方案1】:

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

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

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

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

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

所以这是未定义的行为。

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

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

再次未定义的行为。

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

[...]本国际标准没有要求

短语没有要求似乎也允许任何行为,包括nasal demons。两者都有类似的说明,大致如下:

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

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

代码一定要执行吗?

如果我们阅读Can code that will never be executed invoke undefined behavior?,我们至少可以看到在 C 的情况下存在争论的空间,即必须执行 1 / 0 以调用未定义的行为。更糟糕的是,在 C++ 案例中,behavior 的定义不存在,因此用于 C 案例的部分分析不能用于 C++ 案例。

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

从 C 的角度来看,WG14 defect report 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 会出错 (see it live):

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 是核心常量表达式,除非 e 的求值遵循 抽象机(1.9),将评估以下表达式之一

并包括以下项目符号:

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

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

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

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

虽然,它确实允许:

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

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

【讨论】:

因此,虽然从技术上讲“允许崩溃”,但显示正确的错误消息会更加用户友好。 (但我想这只是编译时可识别错误的一种选择。) @Jongware 是的,但是如果没有任何专门处理它的代码,任何错误都会使编译器崩溃,根据定义,在编译时是可以识别的。 一旦我问了一个关于 behaviour of the compiler when UB is present 的问题,欢迎新的答案...... @ShafikYaghmour 我会从另一个方向处理这个问题。在存在int main(int c, char*v[]) if (!exp1) int y = 1 / exp2; 的情况下,只要编译器不能证明exp1exp2 永远不会同时为0,编译器就必须运行并生成运行代码,因为可以使用只有一个的参数调用程序或其他 0 并且程序员有权期望程序已经生成并在那时工作。当 exp1 和 exp2 是编译时常量时,我​​不明白为什么会有不连续性。 @PascalCuoq 嗯,这是一个有趣的观点,defect report 109 同意你的观点。【参考方案2】:

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

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

1 / 0 仅在评估时为 UB。

C11 标准给出了 explicit example 的 1 / 0 被定义为未评估时的行为:

因此,在接下来的初始化中,

        static int i = 2 || 1 / 0;

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

第 6.6 节,脚注 118。

1 / 0 不是常量表达式。

C11 标准的Section 6.6,在约束下,说

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

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

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的话,那就是废话了。

【讨论】:

OP 应该考虑的另一个例子是void f() int i = 1 / 0; ,它可能出现在一个从不调用f 的严格符合程序中。以防 OP 考虑通过添加简单的死代码分析来尝试修复错误。 如果保证程序流会导致UB,那么整个程序的行为是不确定的。 (但编译器当然不应该崩溃)。 我认为这个答案很好,所有 3 点都是正确的,但我不同意问题上下文中的结论。如果我们假设1 / 0 被评估并且存在UB,那么标准对程序或编译器的行为没有任何要求。虽然崩溃编译器不好,但它仍然是有效的做法。在这种情况下,表达式不是常量表达式是无关紧要的。 @user694733:按照我阅读问题的方式,不能保证表达式会被评估。我想值得添加一个注释,说明如果表达式被保证被评估会发生什么。 @user2357112:尽管实现将源文本转换为可以在闲暇时执行的可执行文件很常见,但标准将“实现”视为接受源代码的东西,将其转换为如果方便,可以使用其他形式,然后执行它。我不认为标准保证翻译结束和执行开始之间有一个“序列点”。【参考方案3】:

来自 C 标准草案 (N1570):

6.5.5 乘法运算符

...

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

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

3.4.3

    未定义的行为 行为,在使用不可移植或错误的程序构造或错误数据时, 本国际标准未对此提出任何要求 注意 可能的未定义行为包括完全忽略与不可预测的情况 结果,在翻译过程中表现或程序执行以记录的方式特征 环境(无论是否发出诊断消息)、终止翻译或 执行(发出诊断消息)。

所以允许编译器崩溃。

【讨论】:

值得注意的是,未定义结构的存在会导致 UB,或者必须能够被调用才能导致它;) 嗯,它没有直接说明,但我会说相反。关于“行为,使用不可移植或错误的程序构造”。在第 3.4 节中,行为被定义为:“外部外观或行为”,其中调用某些未定义的行为,它必须满足行为的要求。编译器必须在它面前证明的地方,至少可以出现,即使不能出现。所以它完成了出现或行动的状态。但从我的角度来看,这只是一种听起来合乎逻辑的解释。 “从标准的措辞看来,在我看来调用是不必要的。要么有 UB,要么没有。” -- 如果抽象机除以零,如果 RHS 为 0/ 运算符将被评估,则存在 UB。如果代码从未尝试除以零,即使死代码中存在除以零,也不会。 UB 在某种意义上是无条件的,如果任何地方都有 UB,那么整个程序都有 UB,编译器可能会崩溃或以其他方式拒绝程序,但是根据 UB 的类型,任何地方是否有 UB 可能取决于可能正在到达 UB 代码。 @hvd 看来你是对的,未评估的代码不一定是 UB,正如 user2357112 的回答所强调的那样。【参考方案4】:

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

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

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

我的编译器正确支持 || 表达式和 && 表达式中的短路。因此,它将 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。

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

【讨论】:

我认为表达式的 RPN 比 AST 更简单、更高效? @fredoverflow 好吧,它有其优点和缺点。它需要很少的内存,很容易在表达式之上添加一元运算符或具有正确操作数的二元运算符,并且很容易从中生成代码(差但功能强大)。但是,将其作为树进行遍历更加困难,对其进行转换也是如此(尤其是插入、删除或旋转节点;数组的所有常见问题)。数组中的 RPN 的一个意想不到的好处可能是您的编译器不需要支持结构(以实现具有互连节点的树)就能够自行编译。【参考方案5】:

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

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

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

【讨论】:

以上是关于在常量表达式中除以零的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript常量和变量表达式 —— 基础语法

编译器错误:函数调用必须在常量表达式中具有常量值

常量表达式 & constexpr

为啥指向函数的“const”指针不能在常量表达式中使用?

C++constexpr和常量表达式

为啥不能在常量表达式中使用 reinterpret_cast? [复制]