2
\$\begingroup\$

I wrote a small header that is supposed to take care of calling the right strtoX or or stoX function for me and doing so at compile time. The code contains two functions: num_cast and string_cast. Each returns a number or string from the argument specified. See main for an example on how to use this.

I'm looking for reviews on the code design/quality and to possibly illuminate a clearer or more concise way of doing this.

#include <string>
#include <iostream>
#include <assert.h>
#include <type_traits>
#include <cstdlib>

template <class T, int B>
constexpr bool string_util_castable()
{
    return (std::is_same<T, int>::value ||
           std::is_same<T, long>::value ||
           std::is_same<T, unsigned long>::value ||
           std::is_same<T, unsigned long long>::value ||
           std::is_same<T, long long>::value ||
           std::is_same<T, float>::value ||
           std::is_same<T, double>::value ||
           std::is_same<T, long double>::value ||
           std::is_same<T, unsigned>::value) &&
           (((B & (B - 1)) == 0 || B == 10) && B >= 0);
}

// overloaded to avoid having a call to std::string constructor and creating
// a std::string for no reason. This consequently means using the heap when 
// it isn't necessary to do so.
template <class T, int B = 10>
T num_cast(const char *param)
{
    static_assert(string_util_castable<T, B>(), "Type and/or base is not supported in num_cast!");

    if (std::is_same<T, int>::value) {
        return static_cast<int>(std::strtol(param, nullptr, B));
    }
    if (std::is_same<T, long>::value) {
        return std::strtol(param, nullptr, B);
    }
    if (std::is_same<T, unsigned long>::value) {
        return std::strtoul(param, nullptr, B);
    }
    if (std::is_same<T, unsigned long long>::value) {
        return std::strtoull(param, nullptr, B);
    }
    if (std::is_same<T, long long>::value) {
        return std::strtoll(param, nullptr, B);
    }
    if (std::is_same<T, float>::value) {
        return std::strtof(param, nullptr);
    }
    if (std::is_same<T, double>::value) {
        return std::strtod(param, nullptr);
    }
    if (std::is_same<T, long double>::value) {
        return std::strtold(param, nullptr);
    }
    if (std::is_same<T, unsigned>::value) {
        return static_cast<unsigned>(std::strtoul(param, nullptr, B));
    }
}

template <class T, int B = 10>
T num_cast(const std::string& param)
{
    static_assert(string_util_castable<T, B>(), "Type and/or base is not supported in num_cast!");

    if (std::is_same<T, int>::value) {
        return std::stoi(param, nullptr, B);
    }
    if (std::is_same<T, long>::value) {
        return std::stol(param, nullptr, B);
    }
    if (std::is_same<T, unsigned long>::value) {
        return std::stoul(param, nullptr, B);
    }
    if (std::is_same<T, unsigned long long>::value) {
        return std::stoll(param, nullptr, B);
    }
    if (std::is_same<T, long long>::value) {
        return std::stoll(param, nullptr, B);
    }
    if (std::is_same<T, float>::value) {
        return std::stof(param, nullptr);
    }
    if (std::is_same<T, double>::value) {
        return std::stod(param, nullptr);
    }
    if (std::is_same<T, long double>::value) {
        return std::stold(param, nullptr);
    }
    if (std::is_same<T, unsigned>::value) {
        return static_cast<unsigned>(std::stoul(param, nullptr, B));
    }
}

template <class T>
std::string string_cast(const T& num)
{
    static_assert(string_util_castable<T, 10>(), "Type and/or base is not supported in string_cast!");
    return std::to_string(num);
}


int main()
{
    auto n = num_cast<signed>("-5");
    auto s = string_cast(10);
    auto ss = num_cast<int>(std::string("100"));

    std::cout << n << "\n";
    std::cout << s << "\n";
    std::cout << ss << "\n";
}
\$\endgroup\$
3
\$\begingroup\$

You have addressed a problem that is annoying many C++ users. Hopefully, some day these generic conversion functions will make it into the standard library.

I don't really see a need for the string_cast function, though. Normal overload resolution would already do the right thing with std::to_string. (Unfortunately, the behavior of std::to_string with regard to floating-point values is less than desirable. See this recent thread on the ISO mailing list for a discussion.)

Because I'm a symmetry fan, I would prefer the other function be named from_string, but never mind that.

I don't see why the base should be a non-type template parameter instead of a function parameter. Granted, often times, the base is already known at compile-time but sometimes it might not and the standard library functions you're forwarding to accept them as run-time parameters anyway.

Your if cascade is a somewhat unusual choice for selecting among several functions depending on a target type. It happens to work in this case because all numbers are implicitly convertible to each other. If you decide to code it this way, I'd recommend you static_casteach return value to the requested type. For the path that will actually be chosen, this will be a no-op. For the other (dead) paths, it will ensure that any spurious compiler warnings about narrowing conversions will be suppressed.

Actually, since the standard library functions don't always return the exact type, I wouldn't use a static_cast but rather check that the value can actually be represented by the target type. You can code this with a custom casting function like this one.

template <typename DstT, typename SrcT>
constexpr std::enable_if_t
<
  (std::is_arithmetic<SrcT>::value && std::is_arithmetic<DstT>::value),
  DstT
>
identity_cast(const SrcT value)
{
  const auto forth = static_cast<DstT>(value);
  const auto back = static_cast<SrcT>(forth);
  return (back == value)
    ? forth
    : throw std::invalid_argument {"Value cannot be represented as target type"};
}

The constexpr declaration only works in C++14 and isn't really needed for the following code but the utility might be useful in other contexts too, where you might want to use it in constant expressions. If the function is evaluated at run-time, the semantics are the same as for a non-constexpr function. If it is evaluated at compile-time and the throw statement is not “executed”, it will be as if it were not there in the first place. If it would be “executed”, though, it will be a compile-time error. I have used the expression form

return (back == value) ? forth : throw std::invalid_argument {"…"};

rather than the more conventional

if (back != value)
  throw std::invalid_argument {"…"};
return forth;

to work around a bug in GCC.

The usual way to select different functions depending on the target type is to use template specialization. In this case, you could define your public conversion functions to forward to a helper struct that is parametrized on the target type.

template <typename T>
std::enable_if_t<std::is_integral<T>::value, T>
from_string(const std::string& text, const int base = 10)
{
  return detail::helper_integer<T>::parse(text, base);
}

template <typename T>
std::enable_if_t<std::is_floating_point<T>::value, T>
from_string(const std::string& text)
{
  return detail::helper_real<T>::parse(text);
}

Note that the version that parses floating-point types does not accept a base argument to avoid confusion.

The primary template for helper_integer and helper_real triggers a compile-time error when instantiated.

template <typename IgnoredT>
struct always_false : std::false_type {};

template <typename T>
struct helper
{
  static_assert(always_false<T>::value, "unsupported type");
  template <typename... ArgTs> static T parse(ArgTs...);
};

template <typename T>
struct helper_integer : helper<T> {};

template <typename T>
struct helper_real : helper<T> {};

The always_false helper is needed to defer the compile-time error until the struct is actually instantiated and not fire already unconditionally when it is parsed.

We now have to specialize helper_integer for all integer types and helper_real for all floating-point types. Since this is a lot of repetitive typing, I have decided to use a macro. You might disagree with this choice.

#define MAKE_HELPER_INTEGER_SPECIALIZATION(TYPE, FUNCTION)      \
    template <>                                                 \
    struct helper_integer< TYPE >                               \
    {                                                           \
      static TYPE                                               \
      parse(const std::string& s, const int b)                  \
      {                                                         \
        return identity_cast< TYPE >(FUNCTION(s, nullptr, b));  \
      }                                                         \
    }

#define MAKE_HELPER_REAL_SPECIALIZATION(TYPE, FUNCTION)         \
    template <>                                                 \
    struct helper_real< TYPE >                                  \
    {                                                           \
      static TYPE                                               \
      parse(const std::string& s)                               \
      {                                                         \
        return FUNCTION(s);                                     \
      }                                                         \
    }

MAKE_HELPER_INTEGER_SPECIALIZATION(signed char,            std::stoi);
MAKE_HELPER_INTEGER_SPECIALIZATION(signed short,           std::stoi);
MAKE_HELPER_INTEGER_SPECIALIZATION(signed int,             std::stoi);
MAKE_HELPER_INTEGER_SPECIALIZATION(signed long int,        std::stol);
MAKE_HELPER_INTEGER_SPECIALIZATION(signed long long int,   std::stoll);

MAKE_HELPER_INTEGER_SPECIALIZATION(unsigned char,          std::stoul);
MAKE_HELPER_INTEGER_SPECIALIZATION(unsigned short int,     std::stoul);
MAKE_HELPER_INTEGER_SPECIALIZATION(unsigned int,           std::stoul);
MAKE_HELPER_INTEGER_SPECIALIZATION(unsigned long int,      std::stoul);
MAKE_HELPER_INTEGER_SPECIALIZATION(unsigned long long int, std::stoull);

MAKE_HELPER_REAL_SPECIALIZATION(float,                     std::stof);
MAKE_HELPER_REAL_SPECIALIZATION(double,                    std::stod);
MAKE_HELPER_REAL_SPECIALIZATION(long double,               std::stold);

#undef MAKE_HELPER_INTEGER_SPECIALIZATION
#undef MAKE_HELPER_REAL_SPECIALIZATION

Note that we also need specializations for the short integer types. I think you forgot them.

Also note the space around helper_integer< TYPE >. It is not really needed here but good practice to avoid bad surprises when TYPE expands to a fully qualified name like ::std::size_t.

<: is a digraph that will be replaced by the token [ thus causing a syntax error.

I have to say that I find the behavior of the standard library functions ignoring any invalid characters that follow the number most unhelpful. In almost any case, I would like to consider this as an error. So you might want to actually use

auto pos = std::size_t {};
const auto value = std::stoi(text, &pos, base);
if (pos != text.length())
  throw std::invalid_argument {"leftover characters"};
return value;

instead of passing the nullptr as second argument.

I think that your overloads taking a raw character string are unnecessary because the standard library functions are not overloaded either. So a temporary std::string object will have to be constructed anyway. On the other hand, taking std::wstring arguments into account might be useful to some. You don't have to replicate the code for this, just templateize on the std::basic_string's character type. Fortunately, the number parsing functions are named the same and simply overloaded on std.:string and std::wstring so you can implement the forwarding call to them in the same template.

As a final note, consider boost::lexical_cast as a ready-made more flexible alternative.

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.