浮点除以零的行为
Posted
技术标签:
【中文标题】浮点除以零的行为【英文标题】:The behaviour of floating point division by zero 【发布时间】:2017-08-13 02:21:39 【问题描述】:考虑
#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。但是我也听说过严格来说是undefined,因此上面的代码不能被认为是可移植的C++。 (理论上这会抹杀我的百万行代码堆栈的完整性。哎呀。)
谁是对的?
请注意,我对定义的实现很满意,但我在这里谈论的是吃猫、打喷嚏未定义的行为。
【问题讨论】:
嗯:“你是说std::cout << a << b << c;
”吗?
@WhiZTiM: 自然 ;-)
@WhiZTiM 我看到了。我们都看到了。很抱歉,您不是出色的 Google ;)
体面的编译器won't even compile your code.
@KerretSB: melpon.org/wandbox/permlink/9c11BxUhoe10vfpr
【参考方案1】:
C++ 标准不强制采用 IEEE 754 标准,因为这主要取决于硬件架构。
如果硬件/编译器正确实现了 IEEE 754 标准,则该部门将提供预期的 INF、-INF 和 NaN,否则......这取决于。
未定义意味着,编译器实现决定,有很多变数,比如硬件架构、代码生成效率、编译器开发者的懒惰等等。
来源:
除以 0.0 的 C++ 标准状态是 undefined
C++ 标准 5.6.4
...如果 / 或 % 的第二个操作数为零,则行为未定义
C++ 标准 18.3.2.4
...static constexpr bool is_iec559;
...56。当且仅当类型符合 IEC 559 标准时为真。217
...57。对所有浮点类型都有意义。
IEEE754的C++检测:
标准库包含一个模板来检测是否支持 IEEE754:
static constexpr bool is_iec559;
#include <numeric>
bool isFloatIeee754 = std::numeric_limits<float>::is_iec559();
如果不支持 IEEE754 怎么办?
视情况而定,通常除以 0 会触发硬件异常并使应用程序终止。
【讨论】:
我不确定您在第二段中的断言。 UB就是UB,好编译器might choose to not compile the code. 嗯,UB 就是 UB,编译器完全有可能假设您不会导致 UB,因此无法达到除法...所以它可能很棘手。我不确定我是否会假设我得到 IEEE-754 划分行为。 @KerrekSB:你不能假设你得到了 IEEE-754 行为,但如果 C++ 告诉你,那么你可以假设它。 IEEE-754 清楚地将除以零指定为 -+INF(如果 Dividend 也为零,则为 NaN)。 语言律师可能会告诉您,std::numeric_limits<T>::is_iec559()
是实现定义的数量,反映了符合 IEC 559 的声明,而不是保证实现正确支持 IEEE 754。语言律师就像那个。
如果编译器没有充分了解目标执行机器以保证 iec559 一致性,它可能不会声称该类型符合 iec559。编译器可以随意违反标准,但就它们而言,它们不是 C++ 编译器,也不是真正的苏格兰人。【参考方案2】:
引用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_iec559
是true
,并且是"usually true
",那么行为是明确定义的并产生预期的结果。
一个相当安全的赌注是扑通一声:
static_assert(std::numeric_limits<double>::is_iec559, "Please use IEEE754, you weirdo");
...靠近您的代码。
【讨论】:
我在 C++ 标准中看不到任何地方证明“除外”子句是合理的。 cppreference 是否假设因为这就是 IEC669 所说的? 请务必引用标准文件,而不是随机网站;) @BoundaryImposition 否,如果行为已定义,则不再未定义。 UB 只是意味着“标准对结果程序的行为没有限制”。但事实并非如此。在其他地方,编译器声称double
的行为符合 IEC559。它可能不会说谎,或者这样做违反了标准。 IEC559 对double
s 的行为施加了限制,独立于 C++ 标准; 1.0/0.0
的结果由 IEC559 定义。通过特征声明 double
is_iec559
也是标准定义的。编译器可以不这样做,但这样做定义了。
@BoundaryImposition 一旦编译器声明is_iec559
,它就会被隐式定义,因为如果编译器声明它必须具有符合iec559的double
s。然后该标准定义了1.0/0.0
的作用。标准确实将iec559标准作为选项纳入范围,一旦“纳入范围”,编译器必须同时符合这两个标准。 C++ 标准对1.0/0.0
的作用没有直接 限制; iec559 标准可以。只有一种方式符合两者。如果声明is_iec559
,则必须支持C++标准下的1.0/0.0
。
@Yakk 假设一个实现使 1.0/0.0 评估为 +Inf 但随机破坏其他内存位。乍一看,这与 C++ 标准一致,即行为未定义,并且与未解决该问题的 IEC559 一致,但显然不是预期的。【参考方案3】:
整数和浮点除以零都是未定义的行为[expr.mul]p4:
二元 / 运算符产生商,二元 % 运算符产生除法的余数 第二个表达式的第一个表达式。 如果 / 或 % 的第二个操作数为零,则行为未定义。 ...
虽然实现可以选择支持Annex F,它具有明确定义的浮点除以零语义。
我们可以从这个 clang 错误报告clang sanitizer regards IEC 60559 floating-point division by zero as undefined 中看到,即使定义了宏 __STDC_IEC_559__,它也是由系统头文件定义的,至少对于 clang 不支持 Annex F 等对于 clang 仍然是未定义的行为:
C 标准的附录 F(IEC 60559 / IEEE 754 支持)定义了 浮点除以零,但 clang(3.3 和 3.4 Debian 快照) 认为它是未定义的。这是不正确的:
对附件 F 的支持是可选的,我们不支持。
#if STDC_IEC_559
这个宏是由您的系统头文件定义的,而不是由我们定义的;这是 系统标头中的错误。 (FWIW、GCC 不完全支持附件 F 要么,IIRC,所以它甚至不是 Clang 特定的错误。)
该错误报告和另外两个错误报告 UBSan: Floating point division by zero is not undefined 和 clang should support Annex F of ISO C (IEC 60559 / IEEE 754) 表明 gcc 在浮点除以零方面符合 附录 F。
虽然我同意 C 库不能无条件地定义 STDC_IEC_559,但问题是特定于 clang 的。 GCC 并不完全支持 Annex F,但至少它的意图是在默认情况下支持它,并且如果舍入模式没有改变,那么除法已经很好地定义了。 现在不支持 IEEE 754 (至少像处理除以零这样的基本功能)被视为不良行为。
这是 gcc Semantics of Floating Point Math in GCC wiki 的进一步支持,这表明 -fno-signaling-nans 是默认值,与 gcc optimizations options documentation 一致,它说:
默认值为 -fno-signaling-nans。
有趣的是,UBSan for clang 默认在 -fsanitize=undefined 下包含 float-divide-by-zero 而gcc does not:
检测浮点除以零。与其他类似选项不同,-fsanitize=undefined 不启用 -fsanitize=float-divide-by-zero,因为浮点除以零可能是获得无穷大和 NaN 的合法方式。
查看live for clang 和live for gcc。
【讨论】:
【参考方案4】:除以 0 是未定义的行为。
来自C++ standard (C++11) 的第 5.6 节:
二元
/
运算符产生商,二元%
运算符 产生第一个表达式除以的余数 第二。 如果/
或%
的第二个操作数为零,则行为为 未定义。 对于整数操作数,/
运算符产生代数 商与丢弃的任何小数部分;如果商a/b
是 以结果类型表示,(a/b)*b + a%b
等于a
。
/
运算符的整数和浮点操作数没有区别。该标准仅规定除以零是未定义的,不考虑操作数。
【讨论】:
请注意,在我的情况下,它不是被零整除。我质疑有效性的原因是%
不适用于 C++ 中的非整数类型。 C++ 不是你知道的 Java。
@Bathsheba 引用的段落没有说明整数与浮点操作数除以零有关的任何内容,只是它未定义。事实上,问题上的一个cmet给出了一个在这种情况下编译错误的例子。
@Bathsheba 引用前的段落对%
产生“例外”: * 和 / 的操作数应具有算术或无范围枚举类型; % 的操作数应为整数或无范围枚举类型。【参考方案5】:
在 [expr]/4 我们有
如果在计算表达式期间,结果未在数学上定义或不在其类型的可表示值范围内,行为未定义。 [注意:大多数现有的 C++ 实现忽略整数溢出。除以零的处理,使用零除数形成余数,所有浮点异常因机器而异,通常可以通过库函数进行调整。 ——尾注]
强调我的
因此,根据标准,这是未定义的行为。它确实继续说,其中一些情况实际上是由实现处理的并且是可配置的。所以它不会说它是实现定义的,但它确实让你知道实现确实定义了一些这种行为。
【讨论】:
【参考方案6】:对于提交者的问题“谁是正确的?”,可以说两个的答案都是正确的。 C 标准将行为描述为“未定义”这一事实并没有规定底层硬件实际执行的操作;它仅仅意味着如果您希望您的程序根据标准有意义,您 - 可能不会假设 - 硬件实际上实现了该操作。但如果你碰巧在执行IEEE标准的硬件上运行,你会发现操作实际上是执行的,结果符合IEEE标准。
【讨论】:
硬件是否支持 IEEE-754 数学无关紧要。扩展双精度类型的设计目标之一是在当今常见的 CPU 上合理高效地处理是有效的。该标准还提到了扩展浮点精度类型的可能性,但将细节留给了实现。太糟糕的语言从未承认过这一点,因为在许多低端微控制器上,一种使用 32 位作为有效位,16 或 32 位作为指数和符号的类型,可能比float
.while 提供更快的处理速度更好的精度。【参考方案7】:
这也取决于浮点环境。
cppreference 有详细信息: http://en.cppreference.com/w/cpp/numeric/fenv (虽然没有例子)。
这应该在大多数桌面/服务器 C++11 和 C99 环境中可用。在所有这些标准化之前,还有一些特定于平台的变体。
我希望启用异常会使代码运行得更慢,因此可能是我所知道的大多数平台默认禁用异常的原因。
【讨论】:
C++ 实际上没有任何 C 的浮点环境。如果你想要一个单一的原因,那就是 C 需要某个 pragma 来启用该环境,而 C++ 没有那个 pragma。 即使在 C 中也会出现 IEEE 浮点异常;它们与 C++ 异常不同。无法禁用浮点异常检测。可以启用或禁用的是异常是否被捕获。捕获浮点异常时会引发 SIGFPE。默认的 SIGFPE 处理程序终止程序。我认为这是一件“好事”,因为它几乎总是表示程序员错误。 (即使错误输入导致 FP 异常,验证输入失败也是编程错误。)让 Infs 和 NaNs 传播会减慢代码速度。以上是关于浮点除以零的行为的主要内容,如果未能解决你的问题,请参考以下文章