【问题标题】:Handling overflow when casting doubles to integers in C在 C 中将双精度转换为整数时处理溢出
【发布时间】:2010-10-06 06:42:45
【问题描述】:

今天,我注意到当我将大于最大可能整数的双精度数转换为整数时,我得到 -2147483648。同样,当我转换一个小于最小可能整数的双精度时,我也会得到 -2147483648。

是否为所有平台定义了这种行为?
检测这种下溢/溢出的最佳方法是什么?将 min 和 max int 的 if 语句放在强制转换之前是否是最佳解决方案?

【问题讨论】:

标签: c++ c casting floating-point


【解决方案1】:

将浮点数转换为整数时,溢出会导致未定义的行为。来自 C99 规范,6.3.1.4 实数浮点和整数部分

当实浮点类型的有限值转换为_Bool以外的整数类型时,小数部分被丢弃(即,该值被截断为零)。如果整数部分的值不能用整数类型表示,则行为未定义。

您必须手动检查范围,但不要使用类似的代码:

// DON'T use code like this!
if (my_double > INT_MAX || my_double < INT_MIN)
    printf("Overflow!");

INT_MAX 是一个整数常量,可能没有精确的浮点表示。与浮点数进行比较时,它可能会舍入到最接近的较高或最近较低的可表示浮点值(这是实现定义的)。例如,对于 64 位整数,INT_MAX2^63 - 1,通常会四舍五入为 2^63,因此检查本质上变为 my_double &gt; INT_MAX + 1。如果my_double 等于2^63,则不会检测到溢出。

例如在 Linux 上使用 gcc 4.9.1,以下程序

#include <math.h>
#include <stdint.h>
#include <stdio.h>

int main() {
    double  d = pow(2, 63);
    int64_t i = INT64_MAX;
    printf("%f > %lld is %s\n", d, i, d > i ? "true" : "false");
    return 0;
}

打印

9223372036854775808.000000 > 9223372036854775807 is false

如果您事先不知道整数和双精度类型的限制和内部表示,就很难做到这一点。但是,如果您将 double 转换为 int64_t,例如,您可以使用精确双精度的浮点常量(假设二进制补码和 IEEE 双精度):

if (!(my_double >= -9223372036854775808.0   // -2^63
   && my_double <   9223372036854775808.0)  // 2^63
) {
    // Handle overflow.
}

构造!(A &amp;&amp; B)也能正确处理NaN。 ints 的便携、安全但略微不准确的版本是:

if (!(my_double > INT_MIN && my_double < INT_MAX)) {
    // Handle overflow.
}

这是出于谨慎的考虑,并且会错误地拒绝等于 INT_MININT_MAX 的值。但对于大多数应用程序来说,这应该没问题。

【讨论】:

  • 我刚刚做了一些经验测试,这个答案似乎是正确的(再次假设二进制补码整数;如果你不能假设,也许 Boost 或 SafeInt 是唯一合理的方法走)。您应该对这个答案投赞成票,对主张 my_double > INT_MAX || 的错误答案投反对票。 my_double
  • @bhaller 我刚刚检查过,Boost 和 SafeInt 都犯了我在回答中讨论的相同错误。
  • 哎呀。您是否向他们报告了问题?
  • @bhaller 是的,herehere
【解决方案2】:

limits.h 具有整数数据类型的最大和最小可能值的常量,您可以在转换之前检查您的双变量,例如

if (my_double > nextafter(INT_MAX, 0) || my_double < nextafter(INT_MIN, 0))
    printf("Overflow!");
else
    my_int = (int)my_double;

编辑:nextafter() 将解决 nwellnhof 提到的问题

【讨论】:

  • 这对我来说不适用于浮动。我尝试使用上面的限制检查来做到这一点float f = INT_MAX; f++; ConvertToInt(f),它不会溢出。有什么不同?
  • @Pittfall: float 有(除了非常奇特的平台都使用 IEEE-754 浮点数)有 24 个有效二进制数字。因此,当您将其设置为 INT_MAX,即 2³¹-1(32 位平台上的 INT_MAX)时,最后一位是 128s。因此,如果您添加任何小于 128 的值,则结果是原始数字,即 (float)INT_MAX + 1.f == (float)INT_MAX。使用double,它的有效数字比int 多。
  • 这个答案是错误的,因为不能保证INT_MININT_MAX 具有精确的浮点表示。以 64 位整数为例,INT_MAX 为 2^63-1,(double)INT_MAX 将四舍五入为 2^63,因此如果my_double 为 2^63,则此检查不会检测到溢出。将检查更改为 my_double &gt;= INT_MAX || my_double &lt; INT_MIN 实际上应该可以使用二进制补码整数,即使它看起来是错误的。
  • 我同意这个答案是错误的。它应该被删除。
  • nextafter 的唯一问题是等于INT_MINdouble 将被检测为溢出,尽管它可以转换为int。除此之外,它还是一个不错的便携解决方案。
【解决方案3】:

回答您的问题:当您投射超出范围的浮点数时的行为是未定义的或特定于实现的。

从经验来看:我曾在 MIPS64 系统上工作过,它根本没有实现这些类型的转换。 CPU 没有做一些确定性的事情,而是抛出了一个 CPU 异常。应该模拟强制转换的异常处理程序返回而不对结果做任何事情。

我最终得到了随机整数。猜猜需要多长时间才能将错误追溯到此原因。 :-)

如果你不确定数字不能超出有效范围,你最好自己做范围检查。

【讨论】:

    【解决方案4】:

    C++ 的一种可移植方式是使用 SafeInt 类:

    http://www.codeplex.com/SafeInt

    该实现将允许对包括强制转换在内的 C++ 数字类型进行正常的加法/减法/等操作。每当检测到溢出情况时,它都会抛出异常。

    SafeInt<int> s1 = INT_MAX;
    SafeInt<int> s2 = 42;
    SafeInt<int> s3 = s1 + s2;  // throws
    

    我强烈建议在溢出是重要场景的任何地方使用这个类。这使得很难避免无声地溢出。如果存在溢出恢复方案,只需捕获 SafeIntException 并根据需要进行恢复。

    SafeInt 现在可以在 GCC 和 Visual Studio 上运行

    【讨论】:

    • 一个粗略的测试让我相信在 C++ 中检测溢出可能是不可能的(没有严重的开销或完全改变范式,例如将每个整数包装为对象)。这个专用类似乎无法处理SafeInt&lt;size_t&gt; x = std::numeric_limits&lt;size_t&gt;::max() + 100(它不会抛出)。
    • 你在开玩笑吗...?您的右侧 did 在到达 SafeInt ctor 之前就已经溢出。这不能怪 SafeInt
    • SafeInt 犯了我在回答中讨论的相同错误。 SafeInt&lt;int64_t&gt; v = pow(2.0, 63.0) 不扔。
    【解决方案5】:

    检测这种下溢/溢出的最佳方法是什么?

    将截断的doubleINT_MIN,INT_MAX 附近的精确 限制进行比较。

    技巧准确将基于INT_MIN,INT_MAX 的限制转换为double 值。 double 可能不完全代表 INT_MAX,因为 int 中的位数可能超过该浮点的精度。*1 在这种情况下,转换的INT_MAXdouble 遭受舍入。 INT_MAX 之后的数字是 2 的幂,当然可以表示为 double2.0*(INT_MAX/2 + 1) 生成大于INT_MAX 的整数。

    这同样适用于非 2s 补码机器上的 INT_MIN

    INT_MAX 始终是 2 - 1 的幂。
    INT_MIN 始终是:
    -INT_MAX(不是 2 的补码)或
    -INT_MAX-1(2 的补码)

    int double_to_int(double x) {
      x = trunc(x);
      if (x >= 2.0*(INT_MAX/2 + 1)) Handle_Overflow();
      #if -INT_MAX == INT_MIN
      if (x <= 2.0*(INT_MIN/2 - 1)) Handle_Underflow();
      #else
    
      // Fixed 2022
      // if (x < INT_MIN) Handle_Underflow();
      if (x - INT_MIN < -1.0) Handle_Underflow();
    
      #endif
      return (int) x;
    }
    

    检测NaN而不使用trunc()

    #define DBL_INT_MAXP1 (2.0*(INT_MAX/2+1)) 
    #define DBL_INT_MINM1 (2.0*(INT_MIN/2-1)) 
    
    int double_to_int(double x) {
      if (x < DBL_INT_MAXP1) {
        #if -INT_MAX == INT_MIN
        if (x > DBL_INT_MINM1) {
          return (int) x;
        }
        #else
        if (ceil(x) >= INT_MIN) {
          return (int) x;
        }
        #endif 
        Handle_Underflow();
      } else if (x > 0) {
        Handle_Overflow();
      } else {
        Handle_NaN();
      }
    }
    

    [Edit 2022] 6 年后更正拐角错误。

    double 范围内的值 (INT_MIN - 1.0 ... INT_MIN)(不包含端点)可以很好地转换为 int。之前的代码失败了。


    *1int 精度大于double 时,这也适用于INT_MIN - 1。尽管这种情况很少见,但这些问题很容易适用于long long。考虑以下之间的区别:

      if (x < LLONG_MIN - 1.0) Handle_Underflow(); // Bad
      if (x - LLONG_MIN < -1.0) Handle_Underflow();// Good
    

    使用 2 的补码,some_int_type_MIN 是 2 的(负)幂并且完全转换为 double。因此x - LLONG_MIN 在关注范围内是精确的,而LLONG_MIN - 1.0 可能会在减法中遭受精度损失。

    【讨论】:

    • 你说2的幂,MAX_INT+1的大小,当然可以表示为double。你能解释一下为什么吗?你的假设是什么?假设 IEEE 就够了吗?
    • 好的,我现在可以看到您假设 FLT_RADIX 是 2 并且 INT_MAX 远低于 DBL_MAX,除了语言标准所保证的之外没有其他任何内容。这是一个奇妙而有创意的解决方案。我喜欢它。
    • 一条评论:你所说的underflow,我相信正式称为negative overflow。下溢是当结果捕捉到零时。粗略地说,溢出是指幅度受到影响。
    • @KristianSpangsege 在 C 中,带有 FP 的 underflowcomment 匹配,C 使用 overflow 来处理超出 int 范围的结果(+ 和-)。在白话中,我听到并阅读了 underflow 的使用结果过于负面,并且由于 OP 以这种方式使用它,因此同样回答也是有意义的。 负溢出是个好词——即使有点冗长。
    • 严格来说还有一个假设,即DBL_MANT_DIG &lt;= DBL_MAX_EXP使得ceil()的结果总是可表示的。 IEEE 类型满足此条件。我从ceil() 的 Linux 手册页中找到了这个。
    【解决方案6】:

    我们遇到了同样的问题。如:

    double d = 9223372036854775807L;
    int i = (int)d;
    

    在 Linux/window 中,i = -2147483648。但在 AIX 5.3 中 i = 2147483647。

    如果双精度超出整数范围。

    • Linux/window 总是返回 INT_MIN。
    • 如果 double 是正数,AIX 将返回 INT_MAX,将返回 INT_MIN double 是负数。

    【讨论】:

    • Linux 绝对不会总是返回 INT_MIN,如果 Windows 这样做,我会感到惊讶,因为这是处理器架构本身的一个功能(它不是特定于操作系统的)。
    • printf("%d\n", (int) (double) (9223372036854775807L));prints 2147483647,但 double d = 9223372036854775807L; int i = (int) d; printf("%d", i); 打印出 -2147483648。 Windows 上的 MinGW730_64
    【解决方案7】:

    另一个选项是使用boost::numeric_cast,它允许在数字类型之间进行任意转换。它会在数字类型转换时检测范围丢失,如果范围不能保留,则抛出异常。

    上面引用的网站还提供了一个小示例,可以快速概述如何使用此模板。

    当然,这不再是普通的 C 了;-)

    【讨论】:

    • boost::numeric_cast 犯了我在回答中讨论的相同错误。 numeric_cast&lt;int64_t&gt;(pow(2.0, 63.0)) 不扔。
    【解决方案8】:

    我不确定这一点,但我认为有可能“打开”下/溢出的浮点异常......看看这个Dealing with Floating-point Exceptions in MSVC7\8,这样你就可以替代 if/else 检查.

    【讨论】:

      【解决方案9】:

      我无法确定它是否适用于所有平台,但这几乎是我使用过的每个平台上发生的情况。除了,根据我的经验,它滚动。也就是说,如果 double 的值为 INT_MAX + 2,那么当强制转换的结果最终为 INT_MIN + 2 时。

      至于处理它的最佳方法,我真的不确定。我自己也遇到过这个问题,但还没有找到一种优雅的方式来处理它。我相信有人会做出回应,对我们俩都有帮助。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-01-01
        • 2013-05-13
        • 2014-04-18
        • 2010-12-31
        • 1970-01-01
        • 2016-10-23
        • 1970-01-01
        相关资源
        最近更新 更多