为什么Clang std :: ostream会写一个std :: istream无法读取的双精度?

Dan*_*iel 13 c++ floating-point gcc iostream clang

我正在使用一个应用程序,用于从文本文件中std::stringstream读取空格分隔的doubles 矩阵.该应用程序使用的代码有点像:

std::ifstream file {"data.dat"};
const auto header = read_header(file);
const auto num_columns = header.size();
std::string line;
while (std::getline(file, line)) {
    std::istringstream ss {line}; 
    double val;
    std::size_t tokens {0};
    while (ss >> val) {
        // do stuff
        ++tokens;
    }
    if (tokens < num_columns) throw std::runtime_error {"Bad data matrix..."};
}
Run Code Online (Sandbox Code Playgroud)

很标准的东西.我努力编写一些代码来制作数据矩阵(data.dat),对每个数据行使用以下方法:

void write_line(const std::vector<double>& data, std::ostream& out)
{
    std::copy(std::cbegin(data), std::prev(std::cend(data)),
              std::ostream_iterator<T> {out, " "});
    out << data.back() << '\n';
}
Run Code Online (Sandbox Code Playgroud)

即使用std::ostream.但是,我发现应用程序无法使用此方法读取我的数据文件(抛出上面的异常),特别是它无法读取7.0552574226130007e-321.

我写了以下最小测试用例,显示了行为:

// iostream_test.cpp

#include <iostream>
#include <string>
#include <sstream>

int main()
{
    constexpr double x {1e-320};
    std::ostringstream oss {};
    oss << x;
    const auto str_x = oss.str();
    std::istringstream iss {str_x};
    double y;
    if (iss >> y) {
        std::cout << y << std::endl;
    } else {
        std::cout << "Nope" << std::endl;
    }
}
Run Code Online (Sandbox Code Playgroud)

我在LLVM 10.0.0(clang-1000.11.45.2)上测试了这段代码:

$ clang++ --version
Apple LLVM version 10.0.0 (clang-1000.11.45.2)
Target: x86_64-apple-darwin17.7.0 
$ clang++ -std=c++14 -o iostream_test iostream_test.cpp
$ ./iostream_test
Nope
Run Code Online (Sandbox Code Playgroud)

我也尝试使用Clang 6.0.1,6.0.0,5.0.1,5.0.0,4.0.1和4.0.0进行编译,但得到了相同的结果.

使用GCC 8.2.0进行编译,代码按照我的预期运行:

$ g++-8 -std=c++14 -o iostream_test iostream_test.cpp
$ ./iostream_test.cpp
9.99989e-321
Run Code Online (Sandbox Code Playgroud)

为什么Clang和GCC之间存在差异?这是一个铿锵的错误,如果没有,应该如何使用C++流来编写可移植的浮点IO?

Sha*_*our 4

我相信 clang 在这里是一致的,如果我们阅读std::stod throws out_of_range error for a string that should be valid 的答案,它会说:

\n
\n

double如果结果在次正常范围内,即使它是可表示的,C++ 标准也允许将字符串转换为报告下溢。

\n

7.63918\xe2\x80\xa210 -313在 的范围内double,但在次正常范围内。C++ 标准规定stod调用strtod,然后按照 C 标准来定义strtod。C 标准指出strtod可能下溢,对此它说 \xe2\x80\x9c如果数学结果的量值太小,以至于在指定类型的对象中无法在没有异常舍入误差的情况下表示数学结果,则结果下溢.\xe2\x80\x9d 这是一个尴尬的措辞,但它指的是遇到次正规值时发生的舍入错误。(低于正常值的相对误差比正常值更大,因此它们的舍入误差可以说是非常大的。)

\n

因此,C++ 标准允许 C++ 实现对于次正规值下溢,即使它们是可表示的。

\n
\n

我们可以确认我们依赖于 [facet.num.get.virtuals]p3.3.4 中的 strtod

\n
\n
    \n
  • 对于双精度值,函数 strtod.
  • \n
\n
\n

我们可以用这个小程序来测试它(实时查看):

\n
void check(const char* p) \n{\n  std::string str{p};\n \n    printf( "errno before: %d\\n", errno ) ;\n    double val = std::strtod(str.c_str(), nullptr);\n    printf( "val: %g\\n", val ) ;\n    printf( "errno after: %d\\n", errno ) ;\n    printf( "ERANGE value: %d\\n", ERANGE ) ;\n \n}\n\nint main()\n{\n check("9.99989e-321") ;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

其结果如下:

\n
errno before: 0\nval: 9.99989e-321\nerrno after: 34\nERANGE value: 34\n
Run Code Online (Sandbox Code Playgroud)\n

7.22.1.3p10中的 C11告诉我们:

\n
\n

这些函数返回转换后的值(如果有)。如果无法执行转换,则返回零。如果正确值溢出并且默认舍入生效(7.12.1),则返回正负 HUGE_VAL、HUGE_VALF 或 HUGE_VALL(根据返回类型和值的符号),并存储宏 ERANGE 的值在错误号中。如果结果下溢 (7.12.1),则函数返回一个值,其大小不大于返回类型中最小的标准化正数;errno 是否获取值 ERANGE 是实现定义的。

\n
\n

POSIX 使用该约定

\n
\n

[ERANGE]
\n要返回的值将导致上溢或下溢。

\n
\n

我们可以通过fpclassify验证它是否是次正规的(实时查看)。

\n

  • @丹尼尔:这是一个错误。它并不违反 C++ 标准,但它是糟糕的设计。它应该被报告为错误,输入流读取输出流的失败是提供错误报告的一个很好的例子,并且应该修复它。 (2认同)