为什么Clang std::ostream写入的double无法被std::istream读取?

14

我正在使用一个应用程序,该应用程序使用std::stringstream从文本文件中读取以空格分隔的double矩阵。该应用程序使用类似以下的代码:

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..."};
}

很标准的操作。我认真编写了代码来生成数据矩阵(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';
}

即使用 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;
    }
}

我在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

我还尝试了使用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

为什么Clang和GCC会有差异?这是clang的一个bug吗?如果不是,那么应该如何使用C++流来编写可移植的浮点数IO?


4
变量名iss用于输出,oss用于输入,这两个名称与内容无关,选择它们有些奇怪且令人困惑。 - πάντα ῥεῖ
1
FYI,1e-320 在次正规范围内:https://en.cppreference.com/w/cpp/language/type - Richard Critten
1
@ShafikYaghmour 嗯,显然 1e-320 在范围 [-DBL_MAX, DBL_MAX] 内,即使它不能被精确表示。 - cpplearner
我已经报告了一个libc++ bug - Daniel
1个回答

4
我相信clang在这方面是符合规范的,如果我们阅读std::stod throws out_of_range error for a string that should be valid的答案,它说:
C++标准允许将字符串转换为double类型时,如果结果在次正常范围内但仍可表示,则报告下溢。7.63918•10^-313在double类型的范围内,但处于次正常范围内。C++标准规定stod调用strtod,然后推迟到C标准定义strtod。C标准指出,如果数学结果的大小太小而无法在指定类型的对象中表示,而不会出现特殊的舍入误差,则strtod可能下溢。这是笨拙的措辞,但它指的是遇到次正常值时出现的舍入误差。(次正常值相对于正常值具有更大的相对误差,因此可以说它们的舍入误差是非同寻常的。)因此,C++实现允许下溢子正常值,即使它们是可表示的。

我们可以确认,我们依赖于{{link1:从[facet.num.get.virtuals]p3.3.4中的strtod}}:

  • 对于double值,使用函数strtod。

我们可以通过这个小程序进行测试(在线查看):

void check(const char* p) 
{
  std::string str{p};
 
    printf( "errno before: %d\n", errno ) ;
    double val = std::strtod(str.c_str(), nullptr);
    printf( "val: %g\n", val ) ;
    printf( "errno after: %d\n", errno ) ;
    printf( "ERANGE value: %d\n", ERANGE ) ;
 
}

int main()
{
 check("9.99989e-321") ;
}

以下是结果:
errno before: 0
val: 9.99989e-321
errno after: 34
ERANGE value: 34

C11中7.22.1.3p10告诉我们:

如果可以进行转换,则函数返回转换后的值。如果无法进行转换,则返回零。如果正确的值溢出并且默认舍入生效(7.12.1),则根据返回类型和值的符号返回plus或minus HUGE_VAL,HUGE_VALF或HUGE_VALL,并将宏ERANGE的值存储在errno中。如果结果下溢(7.12.1),则函数返回一个其大小不大于返回类型中最小规格化正数的值;errno是否获得值ERANGE是实现定义的。

POSIX使用这个约定

[ERANGE]
要返回的值会导致溢出或下溢。

我们可以通过 fpclassify现场演示)验证它是否是次标准的。

2
@Daniel:这是一个bug。虽然它不违反C++标准,但它是糟糕的设计。应该将其报告为一个bug,并且输入流无法读取输出流的故障是提供给bug报告的一个很好的例子,应该予以修复。 - Eric Postpischil
1
strtod 对此输入报告 ERANGE 并不意味着 do_get 被允许设置 failbit - T.C.

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