16

I have a program that reads the coordinates of some points from a text file using std::istringstream, and then it verifies the correctness of parsing by calling stream's operator bool().

In general it works well, but for very small (by magnitude) values the operator returns false in some implementations of the standard library even though the parsing seems to be correct. By "very small" I mean the values below the smallest positive (sub)normal value of float, which are 1.17549e-38f (1.4013e-45f).

Consider the program:

#include <sstream> #include <iostream> void parse( const char * zstr ) { std::istringstream s( zstr ); float f; s >> f; std::cout << (bool)s; } int main() { parse( "1e-30" ); parse( "1e-45" ); parse( "1e-46" ); } 

The program prints

  • 111 with GCC's libstd++,
  • 110 with Microsoft's STL, and
  • 100 with Clang's libc++

Online demo: https://gcc.godbolt.org/z/Tb3cbrPWv

Which implementation is correct here?

15
  • I suggest that you s >> f; -> if (s >> f) std::cout << "GOOD\n"; else std::cerr << "BOOM\n";. And, we see when 0 is printed. Commented Oct 31 at 14:28
  • 2
    Just guessing, but isn't that UB if the value cannot be well represented in a float ? Commented Oct 31 at 14:33
  • 2
    the section that covers this is: timsong-cpp.github.io/cppwp/… Commented Oct 31 at 14:50
  • 3
    operator>> for float is defined in terms of num_get; which in turn is defined in terms of the C function strtof. The C standard has this to say (7.22.1.3/10): "If the result underflows (7.12.1), the functions return a value whose magnitude is no greater than the smallest normalized positive number in the return type; whether errno acquires the value ERANGE is implementation-defined." Emphasis mine. I suspect you are observing this implementation-defined behavior propagating through layers. You could probably test this by calling strtof directly. Commented Oct 31 at 15:24
  • 2
    Results of testing with strtof here. Match yours for clang and MSVC, but not for GCC (where operator>> appears to ignore the underflow). Commented Oct 31 at 15:32

2 Answers 2

18

uff, so,

iostream>>(float&) is specified to be functionally identical to std::use_facet(num_get), which itself is defined to be identical to strtof from <cstdlib>, which refers to the C11 standard, which …

Anyways, In C11 it is defined that the conversion from should be done "correctly rounded". And I'd tend to say that's what GCC does here; and the "should" gives leeway there, anyways. (And rounding a subnormal to 0 seems fine to me!)

But. Does it really matter? you will want to know when a number was too small to be represented sensibly. If I'm right with that assessment: use from_chars instead, make sure to check the error it sets for range errors. Here's a side-by-side comparison with your parse() function to highlight the different behaviours (Compiler Explorer):

#include <charconv> #include <iostream> #include <sstream> #include <string_view> void parse(const char* zstr) { std::istringstream s(zstr); float f; s >> f; std::cout << "parse: " << (bool)s; if (s) { std::cout << " (" << f << ")"; } std::cout << "\n"; } template <typename T> void parse_charconv(const std::string_view& sv) { T value; auto [first_fail, errorcode] = std::from_chars(sv.data(), sv.data() + sv.size(), value); constexpr std::errc noerror{}; std::cout << "charconv (" << typeid(T).name() << "): "; if (errorcode == noerror) { std::cout << "1 (" << value << ")"; } else if (errorcode == std::errc::invalid_argument) { std::cout << "i"; } else if (errorcode == std::errc::result_out_of_range) { std::cout << "R"; } else { std::cout << "?"; } std::cout << "\n"; } int main() { for (const char* str : {"1e-30", "1e-45", "1e-46", "banana pie"}) { std::cout << str << "\n"; parse(str); parse_charconv<float>(str); parse_charconv<double>(str); std::cout << "\n\n"; } } 
Sign up to request clarification or add additional context in comments.

6 Comments

Thanks. I do not care about rounding errors, but I need to know the result of parsing to catch invalid files without numerical data inside. And std::from_chars for floats is great, but unfortunately it was added to libc++ only about a year ago, and not available before Clang 20, so I have to use std::istream.
no, you don't need to fall back to the terribly istream operators! you can still use std::strotof, it's easy! just make a char * end_ptr; float value = std::strotof(sv.data(), &end_ptr); if(end_ptr == sv.data()) { /* a parsing error occurred */ } else { /* value is valid! */ };
sorry, typo, std::strtof, not strotof (but you probably guessed that :))
Thanks, it works for me: gcc.godbolt.org/z/srG1jad87
@Marcus, "rounding a subnormal to 0 seems fine to me" --> So a small normal float like 2.0e-38 gets rounded to the nearest 1e-45 yet a subnormal like 1.0e-38 gets converted to 0.0? Typical IEEE rounding rounds both to the nearest 1e-45. That gradual relative precision loss in rounding of small values incurs less abrupt changes and so is preferable.
yes, preferrable, not arguing with that. Rounding a subnormal still seems fine to me.
7

This is the same issue as in this question except that one is about double instead of float and gets to the conversion through a call to stod instead of through a stream operator. The language standard allows conversion of subnormal values to underflow.

Per C++ 2020 draft N4849 29.7.4.2.2, >> for a float uses num_get. Per 28.4.2.1.2, the conversion is performed (Stage 3) by the rules for strtof from the C library. Per C 2018 7.22.1.3 paragraph 10, the result may underflow as described in 7.12.1. 7.12.1 paragraph 6 tells us “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.”

“Extraordinary roundoff error” is not defined in any of the standards (C++, C, or IEEE-754). However, ordinary rounding error is that which occurs for numbers in the normal range. It is limited by the precision of the floating-point significand: In the normal range, rounding to the nearest representable value never has an error greater than half the distance between representable numbers. With 24-bit significands for float, the relative error is at most one part in 2−24. Below the normal range, the full significand is not available; leading zeros in the significand are used to extend the range of the floating-point format. In this interval, the errors from rounding may exceed one part in 2−24. (In the worst case, it can be 100%, as 2−149 is rounded to 0.) Thus, the error may exceed the ordinary rounding error and can be called extraordinary roundoff error.

Therefore, a C++ implementation is allowed (but not required) by the C++ standard to underflow for subnormal values even though non-zero result could be delivered.

Which implementation is correct here?

They all conform to the C++ standard in this regard, as the standard allows but does not require underflow in this situation.

1 Comment

Thanks! Following your link, one can find libc++ bug report, which is still not resolved: github.com/llvm/llvm-project/issues/38360

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.