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



我正在使用一个应用程序,该应用程序使用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..."};
}

非常标准的东西。我勤奋地编写了一些代码来制作数据矩阵(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 错误吗,如果不是,应该如何使用C++流来编写可移植的浮点 IO?

我相信clang 在这里是一致的,如果我们阅读 std::stod 的答案,就会为应该有效的字符串抛出out_of_range错误,它说:

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

7.63918•10-313double范围内,但在次正常范围内。C++标准说stod调用strtod然后遵从C标准来定义strtod。C 标准指出strtod可能会下溢,关于这一点,它说"如果数学结果的大小太小,以至于数学结果无法在指定类型的对象中表示,如果没有异常舍入误差,则结果下溢。这是尴尬的措辞,但它指的是遇到低于正常值时发生的舍入误差。(次正态值的相对误差大于正常值,因此它们的舍入误差可能非同寻常。

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

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

  • 对于双精度值,函数 strtod。

我们可以用这个小程序进行测试(现场观看(:

void check(const char* p) 
{
std::string str{p};

printf( "errno before: %dn", errno ) ;
double val = std::strtod(str.c_str(), nullptr);
printf( "val: %gn", val ) ;
printf( "errno after: %dn", errno ) ;
printf( "ERANGE value: %dn", ERANGE ) ;

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

其结果如下:

errno before: 0
val: 9.99989e-321
errno after: 34
ERANGE value: 34

7.22.1.3p10 中的 C11 告诉我们:

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

POSIX使用该约定:

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

我们可以通过 fpclassification 验证它是亚正规的(实时查看(。

最新更新