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

183 views Asked by At

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.

2

There are 2 answers

3
Ted Lyngmo On BEST ANSWER

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.
8
user7860670 On

Instead of static assert and type checks you can provide 3 specializations prohibiting generic template.

#include <string>

template<typename T>
T string_to_float(std::string const & s, std::size_t * pos = nullptr) = delete;

template<>
float string_to_float(std::string const & s, std::size_t * pos)
{
    return std::stof(s, pos);
}

template<>
double string_to_float(std::string const & s, std::size_t * pos)
{
    return std::stod(s, pos);
}

template<>
long double string_to_float(std::string const & s, std::size_t * pos)
{
    return std::stold(s, pos);
}

online compiler