c++c++11floating-pointlanguage-lawyeriostream

Parsing of small floats with std::istream


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

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

Which implementation is correct here?


Solution

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