我正在使用一个应用程序,该应用程序使用std::stringstream
从文本文件中读取由double
s分隔的空格矩阵。该应用程序使用的代码有点像:
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?
答案 0 :(得分: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 ++标准允许C ++实现对次标准值进行下溢,即使它们是可表示的。
我们可以确认我们依赖strtod from [facet.num.get.virtuals]p3.3.4:
- 对于双精度值,该函数为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
7.22.1.3p10中的C11告诉我们:
这些函数返回转换后的值(如果有)。如果无法执行转换,则返回零。如果正确的值溢出并且默认舍入有效(7.12.1),则返回加号或减号HUGE_VAL,HUGE_VALF或HUGE_VALL(根据返回类型和值的符号),并存储宏ERANGE的值在errno。 如果结果下溢(7.12.1),则函数返回的值的大小不大于返回类型中最小的标准化正数; errno是否获得ERANGE的值是实现定义的。
POSIX使用that convention:
[ERANGE]
要返回的值将导致上溢或下溢。
我们可以通过fpclassify(see it live)来验证它是否是次正规的。