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";
}
1 Answer 1
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_cast
each 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 template
ize 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.