Search code examples
c++c++20if-constexprc++-attributes

Writing a generic `string_to_float<T>` without overhead compared to `std::stof`


I want to write a function string_to_float with template parameter T such that string_to_float = std::stof, string_to_float = std::stod and string_to_float = std::stold, when T = float, T = double and T = long double, respectively. My attempt was the following:

template<typename T>
T string_to_float(std::string const& s, std::size_t* pos = nullptr)
{
    static_assert(std::is_same_v<T, float> || std::is_same_v<T, double> || std::is_same_v<T, long double>,
        "T is not a built-in floating point type");

    if constexpr (std::is_same_v<T, float>)
        return std::stof(s, pos);
    if constexpr (std::is_same_v<T, double>)
        return std::stod(s, pos);
    if constexpr (std::is_same_v<T, long double>)
        return std::stold(s, pos);

    return T{};
}

However, I worry about the return statement. While the static assertion will already fail in that case, I don't want to produce an additional misleading compiler error when T is not default constructible.

I also want to make sure that the code produced by an invocation of string_to_float is really exactly the same as if I had used std::stof, std::stod or std::stold directly (assuming, of course, that T = float, T = double or T = long double).

This is why I didn't remove the last if-clause checking for T being equal to long double and simply returned std::stold(s, pos); in the last line. On the other hand, at compile time the it is already clear if T = float or T = double and in that case, there would be a return before the return in the last line; so the compiler might ignore that return anyways.

I've also looked at the attribute specifier sequences and hoped that there is some kind of [[unreachable]] attribute so that the compiler really knows that the code below this line will never be reached.


Solution

  • I don't want to produce an additional misleading compiler error when T is not default constructible.

    Then, don't include that last return T{};. It's not ever going to be used anyway.

    Example:

    template<class T>
    inline constexpr bool always_false_v = false;
    
    template <typename T>
    T string_to_float(std::string const& s, std::size_t* pos = nullptr) {
        if constexpr (std::is_same_v<T, float>)
            return std::stof(s, pos);
        else if constexpr (std::is_same_v<T, double>)
            return std::stod(s, pos);
        else if constexpr (std::is_same_v<T, long double>)
            return std::stold(s, pos);
        else 
            static_assert(always_false_v<T>,
                          "T is not a built-in floating point type");
    }
    

    The always_false_v variable template instead of just false is needed for old versions of the compilers that doesn't implement the new rules according to CWG2518.

    Examples (the versions are inclusive):

    • gcc up to 12.3
    • clang up to 16
    • icx up to 2023.1.0
    • msvc still won't be able to deal with just false even in the current latest version.