如何测试无损双/整数转换?

How to test for lossless double / integer conversion?

我有一张双人床,还有一张 int64_t。我想知道它们是否具有完全相同的值,以及将一种类型转换为另一种类型是否不会丢失任何信息。

我当前的实现如下:

int int64EqualsDouble(int64_t i, double d) {
    return (d >= INT64_MIN)
        && (d < INT64_MAX)
        && (round(d) == d)
        && (i == (int64_t)d);
}

我的问题是:这个实现是否正确?如果不是,正确答案是什么? 正确答案必须是没有假阳性,也没有假阴性。

一些示例输入:

是的,您的解决方案可以正常工作,因为它被设计为这样做,因为 int64_t 在使用类似于二进制 IEEE 754 的平台上按定义 (C99 7.18.1.1:1) 表示为二进制补码double 类型的双精度。和this one.

基本一样

在这些条件下:

  • d < INT64_MAX 是正确的,因为它等同于 d < (double) INT64_MAX 并且在转换为双精度时,数字 INT64_MAX,等于 0x7ffffffffffffffff,四舍五入。因此,您希望 d 严格小于结果 double 以避免在执行 (int64_t)d.

  • 时触发 UB
  • 另一方面,INT64_MIN 是 -0x8000000000000000,是可精确表示的,这意味着等于 (double)INT64_MINdouble 可以等于一些 int64_t 并且不应被排除(这样的 double 可以转换为 int64_t 而不会触发未定义的行为)

不言而喻,由于我们专门针对整数和二进制浮点数使用了 2 的补码假设,因此在不同的平台上,这种推理并不能保证代码的正确性。以具有二进制 64 位浮点数和 64 位 1 的补码整数类型 T 的平台为例。在那个平台上 T_MIN-0x7fffffffffffffff。该数字向下舍入为 double,结果为 -0x1.0p63。在该平台上,按照编写的方式使用您的程序,将 -0x1.0p63 用于 d 会使前三个条件为真,从而导致 (T)d 中出现未定义的行为,因为 overflow in the conversion from integer to floating-point is undefined behavior.


如果您可以访问完整的 IEEE 754 功能,则有一个更短的解决方案:

#include <fenv.h>
…
#pragma STDC FENV_ACCESS ON
feclearexcept(FE_INEXACT), f == i && !fetestexcept(FE_INEXACT)

此解决方案利用从整数到浮点数的转换设置 INEXACT 标志 iff 转换不精确(也就是说,如果 i 不能完全表示为 double)。

INEXACT 标志保持未设置且 f 等于 (double)i 当且仅当 fi 在各自的类型中表示相同的数学值。

此方法要求编译器在代码访问 FPU 状态时收到警告,通常使用 #pragma STDC FENV_ACCESS on 但这通常不受支持,您必须改用编译标志。

OP 的代码有一个可以避免的依赖。

为了比较成功,d 必须是整数,round(d) == d 负责处理。即使 d,因为 NaN 也会失败。

d 必须 在数学上 在 [INT64_MIN ... INT64_MAX] 范围内并且如果 if条件适当确保,然后最终 i == (int64_t)d 完成测试。

所以问题归结为比较 INT64 限制与 double d

让我们假设 FLT_RADIX == 2,但不一定 IEEE 754 binary64

d >= INT64_MIN 不是问题,因为 -INT64_MIN 是 2 的幂并且完全转换为相同值的 double,因此 >= 是准确的。

代码想做数学计算 d <= INT64_MAX,但这可能行不通,所以出现问题。 INT64_MAX 是 "power of 2 - 1" 并且 可能 无法准确转换 - 这取决于 double 的精度是否超过 63 位 - 呈现比较不清楚。一种解决方案是将比较减半。 d/2 没有精度损失,INT64_MAX/2 + 1 精确转换为 double 2 的幂

d/2 < (INT64_MAX/2 + 1)

[编辑]

// or simply
d < ((double)(INT64_MAX/2 + 1))*2

因此,如果代码不想依赖精度低于 uint64_tdouble。 (可能适用于 long double 的东西)更便携的解决方案是

int int64EqualsDouble(int64_t i, double d) {
    return (d >= INT64_MIN)
        && (d < ((double)(INT64_MAX/2 + 1))*2)  // (d/2 < (INT64_MAX/2 + 1))
        && (round(d) == d)
        && (i == (int64_t)d);
}

注意:没有舍入模式问题。

[编辑]更深层次的限制解释

数学上的保证,INT64_MIN <= d <= INT64_MAX,可以重新表述为 INT64_MIN <= d < (INT64_MAX + 1),因为我们处理的是整数。由于 (double) (INT64_MAX + 1) 在代码中的原始应用肯定是 0,因此替代方案是 ((double)(INT64_MAX/2 + 1))*2。对于具有 double 更高 2 次幂到 ((double)(INT64_MAX/FLT_RADIX + 1))*FLT_RADIX 的稀有机器,这可以扩展。比较限制是 exact 的 2 次幂,转换为 double 不会造成精度损失,并且 (lo_limit >= d) && (d < hi_limit) 是精确的,无论浮点数的精度如何。注意:带有 FLT_RADIX == 10 的罕见浮点数仍然是一个问题。

除了 Pascal Cuoq 的详尽回答,并考虑到您在评论中提供的额外上下文,我还要添加一个负零测试。你应该保留负零,除非你有充分的理由不这样做。您需要进行特定测试以避免将它们转换为 (int64_t)0。根据您当前的提议,负零将通过您的测试,存储为 int64_t 并作为正零读回。

我不确定测试它们的最有效方法是什么,也许是这样:

int int64EqualsDouble(int64_t i, double d) {
    return (d >= INT64_MIN)
        && (d < INT64_MAX)
        && (round(d) == d)
        && (i == (int64_t)d
        && (!signbit(d) || d != 0.0);
}