I have a number of places in which I wish to use std::enable_if certain templates only if simple static cast from template type A to template type B (both of which are numeric) will not result in any loss of data. However I am not sure what existing type_traits, if any, I should use or if I should write my own.
For example, casting from uint16_t to uint32_t, from float to double, even from int to double is not going to lose any precision or negative sign. But casting from double to int or int to uint32_t would obviously be problematic.
I've monkeyed around a bit, testing is_trivially_constructible, is_assignable, is_constructible, etc etc. but I don't see one that will warn me if I try to go from float to int.
Am I missing something that's in the library currently or should I just write it myself?
(I already know how to write it. It's simple. Just want to make sure I don't reinvent the wheel).
If you make a narrowing conversion intentionally, make your intentions explicit by using a static cast. Otherwise, this error message almost always indicates you have a bug in your code. You can fix it by making sure the objects you initialize have types that are large enough to handle the inputs.
A narrowing conversion changes a value to a data type that might not be able to hold some of the possible values. For example, a fractional value is rounded when it is converted to an integral type, and a numeric type being converted to Boolean is reduced to either True or False .
I am answering my own question this because someone asked me to post my trait and comments don't seem to have formatting.
template <class T, class F>
struct is_safe_numeric_conversion
: pred_base <( ( ( ( std::is_integral<T>::value && std::is_integral<F>::value ) || ( std::is_floating_point<T>::value && std::is_floating_point<F>::value ) ) &&
sizeof(T) >= sizeof(F) ) ||
( std::is_floating_point<T>::value && std::is_integral<F>::value ) ) &&
( ( std::is_signed<T>::value && std::is_signed<F>::value ) || ( std::is_unsigned<T>::value && std::is_unsigned<F>::value ) )>
{
};
Some notes about why I did what I did here:
StackOverflow tells me that someone just gave me points for this today. So I guess people might actually be using it. In that case, I suppose I should present my entire, current version which addresses the flaws I mentioned above.
I'm sure there are better ways to do this and I know C++14/17/etc allow me to do this far less verbosely but I was forced to make this work on VS versions all the way back to VS2012 so I couldn't take advantage of alias templates and the like.
Therefore I did this by writing some helper traits and then composed my final "is_safe_numeric_cast" trait from them. I think it makes things more readable.
// pred_base selects the appropriate base type (true_type or false_type) to
// make defining our own predicates easier.
template<bool> struct pred_base : std::false_type {};
template<> struct pred_base<true> : std::true_type {};
// same_decayed
// -------------
// Are the decayed versions of "T" and "O" the same basic type?
// Gets around the fact that std::is_same will treat, say "bool" and "bool&" as
// different types and using std::decay all over the place gets really verbose
template <class T, class O>
struct same_decayed
: pred_base <std::is_same<typename std::decay<T>::type, typename std::decay<O>::type>::value>
{};
// is_numeric. Is it a number? i.e. true for floats and integrals but not bool
template<class T>
struct is_numeric
: pred_base<std::is_arithmetic<T>::value && !same_decayed<bool, T>::value>
{
};
// both - less verbose way to determine if TWO types both meet a single predicate
template<class A, class B, template<typename> class PRED>
struct both
: pred_base<PRED<A>::value && PRED<B>::value>
{
};
// Some simple typedefs of both (above) for common conditions
template<class A, class B> struct both_numeric : both<A, B, is_numeric> { }; // Are both A and B numeric types?
template<class A, class B> struct both_floating : both<A, B, std::is_floating_point> { }; // Are both A and B floating point types?
template<class A, class B> struct both_integral : both<A, B, std::is_integral> { }; // Are both A and B integral types
template<class A, class B> struct both_signed : both<A, B, std::is_signed> { }; // Are both A and B signed types
template<class A, class B> struct both_unsigned : both<A, B, std::is_unsigned> { }; // Are both A and B unsigned types
// Returns true if both number types are signed or both are unsigned
template<class T, class F>
struct same_signage
: pred_base<(both_signed<T, F>::value) || (both_unsigned<T, F>::value)>
{
};
// And here, finally is the trait I wanted in the first place: is_safe_numeric_cast
template <class T, class F>
struct is_safe_numeric_cast
: pred_base <both_numeric<T, F>::value && // Obviously both src and dest must be numbers
( std::is_floating_point<T>::value && ( std::is_integral<F>::value || sizeof(T) >= sizeof(F) ) ) || // Floating dest: src must be integral or smaller/equal float-type
( ( both_integral<T, F>::value ) && // Integral dest: src must be integral and (smaller/equal+same signage) or (smaller+different signage)
( sizeof(T) > sizeof(F) || ( sizeof(T) == sizeof(F) && same_signage<T, F>::value ) ) )>
{
};
Another possible solution leveraging SFINAE (C++17 required for std::void_t
):
namespace detail
{
template<typename From, typename To, typename = void>
struct is_narrowing_conversion_impl : std::true_type {};
template<typename From, typename To>
struct is_narrowing_conversion_impl<From, To, std::void_t<decltype(To{std::declval<From>()})>> : std::false_type {};
} // namespace detail
template<typename From, typename To>
struct is_narrowing_conversion : detail::is_narrowing_conversion_impl<From, To> {};
Narrowing conversion rules are implicity available with brace initialization. The compiler will report an error when a narrowing cast is required for initialization, e.g. uint8_t{int(1337)}
.
The expression decltype(To{std::declval<From>()})
in is_narrowing_conversion_impl
is ill formed in case of a narrowing cast and will result in the correct value being set for is_narrowing_conversion::value
:
// all following assertions hold:
static_assert(!is_narrowing_conversion<std::int8_t, std::int16_t>::value);
static_assert(!is_narrowing_conversion<std::uint8_t, std::int16_t>::value);
static_assert(!is_narrowing_conversion<float, double>::value);
static_assert( is_narrowing_conversion<double, float>::value);
static_assert( is_narrowing_conversion<int, uint32_t>::value);
Tested with clang, gcc and msvc Example: godbolt
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With