简体   繁体   中英

std::stod throws out_of_range error for a string that should be valid

#include <iostream>
#include <cmath>
#include <sstream>
using namespace std;

int main(){
    stringstream ss;
    double ad = 7.63918e-313;
    ss << ad;
    cout<<ss.str()<<endl;
    //you will see that the above double is valid, and maps to the specified string

    //but stod cannot map it back
    stod("7.63918e-313");
    //terminate called after throwing an instance of 'std::out_of_range'
}

Run it here: https://onlinegdb.com/Sy1MT1iQM

"7.63918e-313" will result from serializing a double, but stod cannot deserialize it. What's going on here? The smallest possible double is supposedly around 10^−324.

Is there a pair of functions somewhere in the stdlib that can reliably map doubles back and forth from stringification? Shouldn't there be?

The plot thickens. We have two bizarre observations.

  • std::numeric_limits<double>::min() cannot be parsed by stod either.

  • std::numeric_limits<double>::min() is not the minimum double. Our double is smaller, and I find we can get smaller doubles by simply dividing min, so it's not that my double is anomalous or anything https://onlinegdb.com/rJvilljQz

I am very concerned.

Converting “7.63918e-313”

The C++ standard allows conversions of strings to double to report underflow if the result is in the subnormal range even though it is representable.

7.63918•10 -313 is within the range of double , but it is in the subnormal range. The C++ standard says stod calls strtod and then defers to the C standard to define strtod . The C standard indicates that strtod may underflow, about which it says “The result underflows if the magnitude of the mathematical result is so small that the mathematical result cannot be represented, without extraordinary roundoff error, in an object of the specified type.” That is awkward phrasing, but it refers to the rounding errors that occur when subnormal values are encountered. (Subnormal values are subject to larger relative errors than normal values, so their rounding errors might be said to be extraordinary.)

Thus, a C++ implementation is allowed by the C++ standard to underflow for subnormal values even though they are representable.

Converting std::numeric_limits::min()

Regarding your observation that std::numeric_limits<double>::min() “cannot be parsed” either (I presume you mean it also reports underflow), this may be due to the fact that you converted std::numeric_limits<double>::min() to a string containing a decimal numeral, and that decimal numeral was not an exact representation of std::numeric_limits<double>::min() . If it was rounded down, it is slightly less than min() , and hence it is also in the subnormal range. Thus, attempting to convert that decimal numeral back to a double may correctly report it is below the normal range.

std::numeric_limits::min() is not the minimum double

Regarding your observation that std::numeric_limits<double>::min() is not the minimum double , that is correct. std::numeric_limits<double>::min() is specified by the C++ standard to be the minimum positive normal value. There may be subnormal values below it.

Normal and subnormal values

For IEEE-754 64-bit binary floating-point, the normal range is from 2 -1022 to 2 1024 -2 971 . Within this range, every number is represented with a signficand (the fraction portion of the floating-point representation) that has a leading 1 bit followed by 52 additional bits, and so the error that occurs when rounding any real number in this range to the nearest representable value is at most 2 -53 times the position value of the leading bit.

In additional to this normal range, there is a subnormal range from 2 -1074 to 2 -1022 -2 -1074 . In this interval, the exponent part of the floating-point format has reached its smallest value and cannot be decreased any more. To represent smaller and smaller numbers in this interval, the significand is reduced below the normal minimum of 1. It starts with a 0 and is followed by 52 additional bits. In this interval, the error that occurs when rounding a real number to the nearest representable value may be larger than 2 -53 times the position value of the leading bit. Since the exponent cannot be decreased any further, numbers in this interval have increasing numbers of leading 0 bits as they get smaller and smaller. Thus the relative errors involved with using these numbers grows.

For whatever reasons, the C++ has said that implementations may report underflow in this interval. (The IEEE-754 standard defines underflow in complicated ways and also allows implementations some choices.)

The value you are trying to read as a double, 7.63918e-313 , is smaller than the minimal value that is representable by a double on your architecture. On my architecture this is 2.22507e-308 and can be obtained using std::numeric_limits<double>::min() .

From the standard: [string.conversions]

Throws out_of_range if strtof , strtod , or strtold sets errno to ERANGE or if the converted value is outside the range of representable values for the return type.

That is to say, mapping doubles back on forth to string is possible using std::to_string and std::stod (or stringstreams) given that they are representable.


Instead you could use Boost.Lexical_Cast for parsing which does not seem to have these limitations.

#include <iostream>
#include <boost/lexical_cast.hpp>

int main() {
    double d = boost::lexical_cast<double>("7.63918e-313");
    std::cout << d << "\n";
}

Live example

Another alternative is Boost.Spirit .

#include <iostream>
#include <boost/spirit/home/x3.hpp>

namespace x3 = boost::spirit::x3;

int main() {
    double d = 0;
    std::string input = "7.63918e-313";
    x3::parse(input.begin(), input.end(), x3::double_, d);
    std::cout << d << "\n";
}

Live example

According to the specification of strtod (which is referred from the specification of std::strod ), if the conversion underflows , the function is allowed, but not required to set errno to ERANGE . It is implementation-defined whether errno is set in this case.

In your example the conversion does underflow. Apparently, in your implementation strtod sets errno to ERANGE and std::stod unconditionally throws std::out_of_range when it sees that ERANGE in errno .

In a different implementation strtod might not set errno to ERANGE in this case, and std::stod would not throw.

Basically, C++ standard library is not guaranteed to properly convert underflowing values. Even if strtod decides not to set errno , it is still allowed to return the smallest normalized value as a result in this case, instead of your original value.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM