在不丢失精度但具有最少位数的情况下格式化双精度数

Format a double without losing precision but with a minimum number of digits

在我们的 C++ 代码库中,我们有一个默认的格式化方法,可以将 double 浮点数转换为字符串,特别用于 JSON 序列化和调试日志。对于默认的数字格式,我有以下相互矛盾的要求:

  1. 有利于人类的可读性。 1000 优于 1e30.125 优于 1.25e-1.
  2. 保持精度。比 3.14.
  3. 更喜欢 3.1415926535897931
  4. 避免十进制数字出现虚假数字。比 0.10000000000000001.
  5. 更喜欢 0.1

到目前为止,我发现的最佳权衡是使用与 printf("%.15g", value) 格式等效的方法。它满足 13 的要求,但不完全 2。大约有 4 位的精度损失。

其他人使用基于 "%.17g" 的默认格式,它满足 12 的要求,但不满足 3。例如,数字 0.2 的格式为 0.20000000000000001.

在两者之间,格式 "%.16g" 接近于满足 23 的要求,但并不总是满足两者的要求。

作为示例,我希望0.3格式化为0.3,但是0.1+0.2,这有点由于舍入误差较大,将格式化为 0.30000000000000004 以查看差异。

我编写了以下函数,以我希望的方式格式化浮点数,作为概念证明。然而,从性能的角度来看,这是不可接受的,因为它可以在 double 和字符串之间进行多达 34 次转换,与当前使用 "%.15g".

的实现相比,精度增益有限。
std::string doubleToString(double number)
{
    char buffer[32];
    long long intVal = static_cast<long long>(number);
    if(intVal == number)
    {
        sprintf(buffer, "%lld", intVal);
    }
    else
    {
        for(int i=1; i<=17; i++)
        {
            sprintf(buffer, "%.*g", i, number);
            double readBack = atof(buffer);
            if(readBack == number)
                break;
        }
    }
    return buffer;
}

我刚刚意识到 Python 已经按照我想要的方式格式化数字:

$ python3
Python 3.8.6 (default, Oct  8 2020, 14:06:32) 
[Clang 12.0.0 (clang-1200.0.32.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 0.3
0.3
>>> 0.1+0.2
0.30000000000000004
>>> 

有没有一种方法可以在不牺牲太多性能的情况下在 C++ 中实现相同的行为?

在 Frodyne 发表评论后,我找到了一个非常简单快速的解决方案。 C++17 std::to_chars 函数默认格式化浮点数以满足最短往返 要求。这意味着所有不同的浮点数在序列化后仍然不同,并且要格式化的字符数最少。 所以转换在标准C++17中可以这样写。

#include <charconv>
#include <string>

std::string doubleToString(double number)
{
    char buffer[24];
    std::to_chars_result err = std::to_chars(buffer, buffer+sizeof(buffer), value);
    return std::string(buffer, err.ptr);
}

Microsoft 讲座的好消息是,除了解决最短往返 问题外,MSVC 中的实现速度非常快!它基于令人难以置信的 Ryu algorithm.

坏消息是,在撰写本文时,std::to_chars 仅适用于 Microsoft 工具链中的浮点数。 Clang libc++GCC libstdc++ 中的实现目前仅限于整数。