Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoiding narrowing conversions with C++ type_traits

Tags:

c++

typetraits

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).

like image 951
Joe Avatar asked Mar 28 '16 20:03

Joe


People also ask

How do you fix narrowing conversions?

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.

What is a narrowing conversion?

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 .


2 Answers

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:

  • I ended up using sizeof to check the actual sizes of types instead of numeric_limits::max / lowest. I don't like that and would have preferred to use numeric_limits but Visual C++ was giving me fits about that. I'm wondering of perhaps it's because their constexpr implementation doesn't work in some of the versions I'm using.
  • I use my own little "pred_base" just to make things less verbose. I realize I could have used integral_constant for that
  • Just today I realized that this disallows a valid conversion from a small unsigned type (say, uint8_t) to a large signed signed type (say int64_t) even though the latter can easily hold all possible values of the former. I need to fix that but it's minor and at this point, I think I'm the only one still interested in this...

FINAL VERSION (edited 3-FEB-2018)

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 ) ) )>
{
};
like image 64
Joe Avatar answered Nov 07 '22 11:11

Joe


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

like image 25
Quxflux Avatar answered Nov 07 '22 11:11

Quxflux