strtoull()函数的输出在转换为double类型并再次转换为uint64_t类型时会失去精度。

8

请考虑以下内容:

#include <iostream>
#include <cstdint>

int main() {
   std::cout << std::hex
      << "0x" << std::strtoull("0xFFFFFFFFFFFFFFFF",0,16) << std::endl
      << "0x" << uint64_t(double(std::strtoull("0xFFFFFFFFFFFFFFFF",0,16))) << std::endl
      << "0x" << uint64_t(double(uint64_t(0xFFFFFFFFFFFFFFFF))) << std::endl;
   return 0;
}

这将打印:

0xffffffffffffffff
0x0
0xffffffffffffffff

第一个数字只是将 ULLONG_MAX 从字符串转换为uint64_t的结果,这如预期一样工作。

然而,如果我将结果强制转换为double,然后再转换回uint64_t,那么它会打印出0,即第二个数字。

通常,我会将此归因于浮点数的精度不准确性,但更让我感到困惑的是,如果我将ULLONG_MAXuint64_t转换为double,然后再转换回uint64_t,结果就是正确的(第三个数字)。

为什么第二个和第三个结果之间有差别?

编辑(由@Radoslaw Cybulski) 要尝试另一个“发生了什么”,请使用以下代码:

#include <iostream>
#include <cstdint>
using namespace std;

int main() {
    uint64_t z1 = std::strtoull("0xFFFFFFFFFFFFFFFF",0,16);
    uint64_t z2 = 0xFFFFFFFFFFFFFFFFull;
    std::cout << z1 << " " << uint64_t(double(z1)) << "\n";
    std::cout << z2 << " " << uint64_t(double(z2)) << "\n";
    return 0;
}

这段代码愉快地打印出:

18446744073709551615 0
18446744073709551615 18446744073709551615

一个线索表明这是未定义的行为:在本地测试中,使用g++版本6.3时,行为因是否传递优化标志而异。当我传递-O1-O2-O3时,我与您的行为匹配。当我不传递优化标志(或显式传递-O0)时,两个往返转换的结果都是0(检查汇编代码,只有-O0实际上对z2执行转换;基于标准规定转换会产生差异的任何情况都是未定义的行为,参见eerorika的答案)。 - ShadowRanger
澄清检查Radoslaw代码汇编的结果:在-O1及更高级别时,z2实际上从未存在过;在打印之前,立即将0xffffffffffffffff的立即值直接加载到参数寄存器中,而不会存储在专用寄存器或堆栈位置中。只有在-O0(避免干扰调试的优化;它尝试保持代码行和相关汇编之间的对应关系)时,才会为z2创建堆栈位置,在每次使用时从中加载,对该值执行强制转换等操作。 - ShadowRanger
1个回答

10
离0xFFFFFFFFFFFFFFFF最近且可由double表示(假设64位IEEE),其值为18446744073709551616。你会发现这个数比0xFFFFFFFFFFFFFFFF还要大。因此,该数字超出了uint64_t的表示范围。
关于将浮点数转换回整数,标准规定如下(引用最新草案):
[conv.fpint] 浮点类型的prvalue可以转换为整数类型的prvalue。转换将截断小数部分。如果截断的值无法在目标类型中表示,则行为未定义。
第二个和第三个结果之间的差异是为什么呢?
因为程序的行为是未定义的。
虽然分析UB差异的原因基本上是没有意义的,因为变化的范围是无限的,但我猜测在这种情况下差异的原因是,在一个情况下该值是编译时常量,而在另一个情况下有一个在运行时调用的库函数。

我想知道,为什么转换要针对最接近的大整数而不是小整数? - Stack Danny
3
可能是因为较大的双精度数比较接近,而较小的不太接近。结果会取决于当前的舍入模式。 - eerorika
4
不需要再为此发布另一个答案,但你可能想提到的是,对于标准 IEEE 754 双精度二进制浮点数,它仅具有 53 位整数级精度。UINT64_C(1) << 53 是最后一个连续的整数值,可以无损地转换为 double 并返回;超过该限制的任何奇数值将舍入(随着您越来越多地依赖指数将整数分量放大,不可被4、8、16等整除的值也会最终舍入)。 - ShadowRanger

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