浮点除零的行为

59

请考虑

#include <iostream>
int main()
{
    double a = 1.0 / 0;
    double b = -1.0 / 0;
    double c = 0.0 / 0;
    std::cout << a << b << c; // to stop compilers from optimising out the code.    
}

我一直认为a将会是+Inf,b将会是-Inf,c将会是NaN。但我也听说过有种传言,严格来讲,浮点数除以零的行为是未定义的,因此上面的代码不能被视为可移植的C++。(理论上这会摧毁我超过百万行的代码堆栈的完整性。哎呀!)

谁是正确的?

注意,我对实现定义感到满意,但我所说的是猫吃、恶魔打喷嚏般的未定义行为


1
你是不是想说 std::cout << a << b << c; - WhiZTiM
1
@WhiZTiM:自然地;-) - Bathsheba
2
@WhiZTiM 我看到了。我们都看到了。很抱歉,但你不是一个伟大的 Google ;) - Quentin
5
优秀的编译器甚至不会编译你的代码。 - Kerrek SB
1
@KerretSB: http://melpon.org/wandbox/permlink/9c11BxUhoe10vfpr - Adrian Maire
显示剩余16条评论
7个回答

43

C++标准不强制使用IEEE 754标准,因为这主要取决于硬件架构。如果硬件/编译器正确实现了IEEE 754标准,则除法将提供预期的INF、-INF和NaN;否则,情况就不确定了。

未定义的含义是编译器实现决定的,其中有许多变量,例如硬件架构、代码生成效率、编译器开发人员的懒惰等。

C++标准规定,对0.0的除法是未定义行为。

C++检测IEEE754:

标准库包括一个模板,用于检测是否支持IEEE754:


static constexpr bool is_iec559;

#include <numeric>
bool isFloatIeee754 = std::numeric_limits<float>::is_iec559();

如果不支持IEEE754会发生什么?

这取决于情况,通常被0除会触发硬件异常并导致应用程序终止。


4
我不确定你在第二段所做的论述。UB就是UB,好的编译器可能会选择不编译该代码。 - Kerrek SB
1
好的,未定义行为就是未定义行为,编译器假设你不会引起未定义行为,因此可能无法到达除法运算...所以这可能会很棘手。我不确定是否应该假设我能获得IEEE-754除法行为。 - Kerrek SB
9
一个精通语言规则的专家可能会告诉您,std::numeric_limits<T>::is_iec559()是一个实现定义的量,反映了符合IEC 559标准的声明,而不是实现正确支持IEEE 754的保证。这就是语言专家们的做法。 - Peter
5
如果编译器不足以保证符合iec559标准,则不能声称该类型符合iec559标准。编译器可以违反标准,但只要违反了标准,它们就不是C++编译器,也不是真正的苏格兰人。 - Yakk - Adam Nevraumont
1
编译器通常会记录执行环境的要求。编译器无需记录在违反其声明的要求的环境中运行可执行文件时会发生什么。如果编译器声称符合IEC559,并且还记录所生成的代码只适用于IEC559兼容硬件,那么它在其他任何平台上运行时,就没有义务遵守任何特定的行为义务。 - supercat
显示剩余23条评论

26

引用自 cppreference:

如果第二个操作数为零,则行为是未定义的,除非正在进行浮点除法并且该类型支持 IEEE 浮点算术(参见 std::numeric_limits::is_iec559),那么:

  • 如果一个操作数是 NaN,则结果为 NaN

  • 将非零数除以 ±0.0 会得到正确符号的无穷大并引发 FE_DIVBYZERO

  • 将 0.0 除以 0.0 会得到 NaN,并引发 FE_INVALID

这里讨论的是浮点除法,所以实际上是实现定义了是否将 double 除以零视为未定义行为。

如果 std::numeric_limits<double>::is_iec559true,而且它通常是 "true 的",那么行为是明确定义的,并产生预期的结果。

一个相当安全的选择是使用:

static_assert(std::numeric_limits<double>::is_iec559, "Please use IEEE754, you weirdo");

...在您的代码附近。


4
我在C++标准中找不到任何证明“except”子句的地方。cppreference是不是认为这是因为IEC669这样说? - Martin Bonner supports Monica
3
请引用标准文件,而不是随意的网站 ;) - Lightness Races in Orbit
1
@BoundaryImposition 不,如果行为被定义了,它就不再是未定义的。UB只是意味着“标准对结果程序的行为没有限制”。而且确实如此。在其他地方,编译器声称double的行为符合IEC559。它可能不会撒谎,否则它就违反了标准。IEC559独立于C++标准对double的行为施加限制;1.0/0.0的结果由IEC559定义。通过特性声明double is_iec559也是标准定义的。编译器可以选择不这样做,但这样做就是定义的。 - Yakk - Adam Nevraumont
2
@BoundaryImposition 当编译器声明 is_iec559 时,它会隐式地定义一次,因为如果声称支持 iec559,则编译器必须具有符合 iec559 的 double。然后该标准定义了 1.0/0.0 的行为。该标准将 iec559 标准作为 选项 引入范围,一旦被 "引入范围",编译器必须符合两个标准。C++ 标准对 1.0/0.0 的行为没有 直接 限制;iec559 标准则有。只有一种方法可以同时符合 两个 标准。如果声称支持 is_iec559,则必须在 C++ 标准下支持 1.0/0.0 - Yakk - Adam Nevraumont
2
@Yakk 假设一种实现使得 1.0/0.0 的计算结果为 +Inf,但会破坏其他随机内存位。乍一看,这似乎符合 C++ 标准中未定义的行为,也符合未涉及此问题的 IEC559 标准,但显然不是预期的结果。 - user743382
显示剩余10条评论

14

除数为零时,整数和浮点数的除法都是未定义行为 [expr.mul]p4:

二元运算符 / 返回商,二元运算符 % 返回第一表达式除以第二个表达式的余数。如果 / 或 % 的第二个操作数为零,则行为是未定义的。...

虽然实现可以选择支持Annex F,该附件具有浮点除零的明确定义的语义。

我们可以从这个clang bug报告中看到clang sanitizer将IEC 60559浮点除零视为未定义的,即使宏__STDC_IEC_559__被定义,它仍然被系统头文件定义,并且至少对于clang不支持Annex F,因此对于clang来说仍然是未定义的行为:

C标准的Annex F(IEC 60559 / IEEE 754支持)定义了浮点除以零,但是clang(3.3和3.4 Debian快照)将其视为未定义。这是不正确的:

支持附件F是可选的,我们不支持它。

#if STDC_IEC_559

这个宏是由系统头文件定义的,而不是我们定义的。这是你的系统头文件中的一个错误。(顺便说一句,据我所知,GCC也没有完全支持Annex F,所以这甚至不是Clang特有的bug。)

那个bug报告和其他两个bug报告UBSan:浮点数除以零不是未定义的clang 应该支持 Annex F of ISO C (IEC 60559 / IEEE 754)表明gcc在浮点除以零方面遵循了Annex F

虽然我同意C库无条件地定义STDC_IEC_559的做法是不对的,但这个问题是特定于clang的。GCC并没有完全支持Annex F,但至少它的意图是默认支持它,并且如果未改变舍入模式,则其除法是良定义的。现在不支持IEEE 754(至少基本特性,如处理除以零)被认为是不好的行为。

这得到了 GCC GCC浮点数学语义维基 的进一步支持,该维基表明 -fno-signaling-nans 是默认设置,与GCC优化选项文档相符合:

默认值是“-fno-signaling-nans”。

有趣的是,UBSan用于Clang,默认情况下在-fsanitize=undefined下包括float-divide-by-zero,而GCC则没有

检测浮点数除以零。与其他类似的选项不同,-fsanitize = float-divide-by-zero未启用 -fsanitize = undefined,因为浮点数除以零可能是获得无穷大和NaN的合法方法。

Clang中实时查看,在GCC中实时查看。


这个宏是由你的系统头文件定义的,而不是我们定义的。这是Unix和类Unix系统编译器长期存在的问题。ISO C定义了#include <stdio.h>的工作方式,但没有描述确切的内容。声称符合ISO标准的编译器不能随意获取任何名为stdio.h的文件,并希望其中包含正确的内容。 - MSalters

8

除以0是未定义行为

来自C++标准(C++11)第5.6节:

二进制运算符/返回商,二进制运算符%返回第一个表达式除以第二个表达式的余数。如果/%的第二个运算符为零,则行为未定义。对于整数操作数,/运算符返回代数商,并舍去任何小数部分;如果商a/b在结果类型中可表示,则(a/b)*b + a%b等于a

/运算符不区分整数和浮点数操作数。标准只声明了除以零是未定义的,而不考虑操作数。


3
请注意,对于我的情况,这不是整数除以零。我质疑其有效性的原因是在C++中,模运算符“%”不适用于非整数类型。C++可不是Java,你知道的。 - Bathsheba
1
@Bathsheba 引用的段落并没有关于整数与浮点数操作数在除以零时的区别,只是说这是未定义的。实际上,问题的评论之一给出了一个编译错误的例子。 - dbush
1
@Bathsheba 在引用的段落之前有一个“例外”,适用于乘法和除法的操作数应具有算术或未作用域枚举类型;%的操作数应具有整数或未作用域枚举类型。 - pipe

6
在[expr]/4中,我们有:
如果在表达式的计算过程中,结果在数学上未定义或不在其类型可表示值范围内,则行为是未定义的。[注意:大多数现有的C++实现忽略整数溢出。除以零,使用零除数形成余数和所有浮点异常的处理因机器而异,并且通常可以通过库函数进行调整。-end note]
强调我的。
因此,根据标准,这是未定义的行为。它确实继续说,一些情况实际上由实现处理并且可配置。因此,它不会说这是实现定义的,但它确实让您知道实现定义了某些行为。

1
对于提交者的问题“谁是正确的?”,可以完全可以说两个答案都是正确的。 C标准将行为描述为“未定义”并不意味着底层硬件实际执行了什么操作;它仅仅表示如果你希望你的程序根据标准有意义,你-不能假设-硬件实际上执行了该操作。但是,如果您恰好在实施IEEE标准的硬件上运行,则会发现该操作实际上已经实现,并且其结果符合IEEE标准的规定。

1
硬件是否支持IEEE-754数学并不重要。扩展双精度类型的设计目标之一是在当时的常见CPU上高效处理。标准还提到了扩展浮点精度类型的可能性,但将细节留给了实现。很遗憾语言从未承认过它,因为在许多低端微控制器上,使用32位表示尾数和16或32位表示指数和符号的类型可以比float更快地处理,同时提供更好的精度。 - supercat

0

这也取决于浮点环境。

cppreference有详细信息: http://en.cppreference.com/w/cpp/numeric/fenv (没有示例)。

这应该在大多数桌面/服务器C++11和C99环境中可用。还有特定于平台的变化,早于所有这些标准化。

我希望启用异常会使代码运行更慢,因此出于这个原因,我所知道的大多数平台默认禁用异常。


C++实际上没有C的浮点环境。如果你想要一个单一的原因,那就是C需要某个特定的编译指示来启用该环境,而C++没有这个编译指示。 - Kerrek SB
IEEE浮点异常即使在C语言中也会发生;它们与C++异常不同。无法禁用浮点异常的检测。可以启用或禁用的是是否捕获异常。当捕获到浮点异常时,将引发SIGFPE。默认的SIGFPE处理程序终止程序。我认为这是一件“好事”,因为它几乎总是表示程序员错误。(即使FP异常是由于错误的输入导致的,未验证输入是编程错误。)让Infs和NaNs传播会使代码变得非常缓慢。 - David Hammen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接