0

I'm looking for a C++ feature/code, which I feel is very basic and demanded for generic programming: an ability to tell if some value of given numeric type (integer or float) is exactly representable by another given numeric type.

Long story short, I have a number of template classes, which operate on arbitrary numeric types and they have to interoperate properly. For instance, a memory stream may work with 32bit size type, but file stream has 64bit size type and passing data between them may result in overflow. Another example is geometry classes, one parametrized to work with 64bit floats, another parametrized to work with 64bit integers: passing data between them may result both in rounding errors (integer can't represent fractions) and overflow (integer has bigger value range than same-sized float).

So I wonder if should do my own implementation, or a solution is readily available (unfortunately, I can't use heavyweight libraries like Boost)?

Update: while typing my question, I decided to roll out my own implementation:

#include <limits>
#include <array>

enum class NumberClass {
    UNKNOWN, INTEGER, FLOAT
};

template <typename T> struct NumberClassResolver {
    static const NumberClass value = std::numeric_limits<T>::is_integer ? NumberClass::INTEGER : (std::numeric_limits<T>::is_iec559 ? NumberClass::FLOAT : NumberClass::UNKNOWN);
};

template <typename From, typename To> struct StaticIntegerCastCheck {
    static const bool value = std::is_same<From, To>::value || (sizeof(From) < sizeof(To)) || ((std::numeric_limits<From>::is_signed == std::numeric_limits<To>::is_signed) && (sizeof(From) <= sizeof(To)));
};
template <typename From, typename To> struct StaticFloatCastCheck {
    static const bool value = std::is_same<From, To>::value || (sizeof(From) < sizeof(To)); // NOTE Here we rely on the fact, that floats are IEEE-754 compliant and bigger ones has both bigger significand and exponent
};


template <NumberClass FromClass, NumberClass ToClass, typename From, typename To> struct StaticNumberCastCheckHelper;
template <typename From, typename To> struct StaticNumberCastCheckHelper<NumberClass::INTEGER, NumberClass::INTEGER, From, To> {
    static const bool value = StaticIntegerCastCheck<From, To>::value;
};
template <typename From, typename To> struct StaticNumberCastCheckHelper<NumberClass::INTEGER, NumberClass::FLOAT, From, To> {
    static const bool value = sizeof(To) > sizeof(From); // NOTE Here we rely on assumption, that sizes are POTs and significand part of a float never takes up less that half of it's size
};
template <typename From, typename To> struct StaticNumberCastCheckHelper<NumberClass::FLOAT, NumberClass::INTEGER, From, To> {
    static const bool value = false;
};
template <typename From, typename To> struct StaticNumberCastCheckHelper<NumberClass::FLOAT, NumberClass::FLOAT, From, To> {
    static const bool value = StaticFloatCastCheck<From, To>::value;
};

template <typename From, typename To> struct StaticNumberCastCheck {
    static const bool value = StaticNumberCastCheckHelper<NumberClassResolver<From>::value, NumberClassResolver<To>::value, From, To>::value;
};

template <bool isFromSigned, bool isToSigned, typename From, typename To> struct RuntimeIntegerCastCheckHelper {
    static bool check(From value) {
        return (value >= std::numeric_limits<To>::min()) && (value <= std::numeric_limits<To>::max()); // NOTE Compiler must eliminate comparisson to zero for unsigned types
    }
};

template <bool isStaticallySafeCastable, NumberClass FromClass, NumberClass ToClass, typename From, typename To> struct RuntimeNumberCastCheckHelper {
    static bool check(From value) {
        return false;   
    }
};
template <NumberClass FromClass, NumberClass ToClass, typename From, typename To> struct RuntimeNumberCastCheckHelper<true, FromClass, ToClass, From, To> {
    static bool check(From value) {
        return true;    
    }
};
template <typename From, typename To> struct RuntimeNumberCastCheckHelper<false, NumberClass::INTEGER, NumberClass::INTEGER, From, To> {
    static bool check(From value) {
        return RuntimeIntegerCastCheckHelper<std::numeric_limits<From>::is_signed, std::numeric_limits<To>::is_signed, From, To>::check(value);
    }
};
template <typename From, typename To> struct RuntimeNumberCastCheckHelper<false, NumberClass::FLOAT, NumberClass::FLOAT, From, To> {
    static bool check(From value) {
        To toValue = static_cast<To>(value);
        From fromValue = static_cast<From>(toValue);
        return value == fromValue;
    }
};
template <typename From, typename To> struct RuntimeNumberCastCheckHelper<false, NumberClass::INTEGER, NumberClass::FLOAT, From, To> {
    static bool check(From value) {
        To toValue = static_cast<To>(value);
        From fromValue = static_cast<From>(toValue);
        return value == fromValue;
    }
};
template <typename From, typename To> struct RuntimeNumberCastCheckHelper<false, NumberClass::FLOAT, NumberClass::INTEGER, From, To> {
    static bool check(From value) {
        To toValue = static_cast<To>(value);
        From fromValue = static_cast<From>(toValue);
        return value == fromValue;
    }
};

template <typename From, typename To> struct RuntimeNumberCastCheck {
    static bool check(From value) {
        return RuntimeNumberCastCheckHelper<StaticNumberCastCheck<From, To>::value, NumberClassResolver<From>::value, NumberClassResolver<To>::value, From, To>::check(value);
    }
};

Intended usage:

StaticNumberCastCheck<uint8_t, float>::value; // can we safely cast all 8-bit unsigned integers to float?
RuntimeNumberCastCheck<float, uint8_t>::check(42); // can we safely cast 42 from float to 8-bit unsigned integer?

It's based on some broad assumptions, so I also would be glad to hear if I'm missing something here (false positives, optimizations, etc).

Dmitry
  • 1,230
  • 10
  • 19

2 Answers2

2

This information can be found in the documentation of the std::numeric_traits template, which gives you each integer or floating point type's minimum and maximum values (and whether the given numerical type is integer or floating point).

Generally, an integer type obviously can't express every floating value, so your first order of business would be to check is_exact. That'll square away comparions between integer and floating point types. Then, you can compare min() and max(), to see if the given numeric type covers the range of another numeric type.

Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148
1
template<class From, class To>
using safe_convert_result_t = decltype(To{std::declval<From>()});

namespace details {
  template<class...>struct voider{using type=void;};
  template<class...Ts>using void_t=typename voider<Ts...>::type;

  template<template<class...>class Z, class, class...Ts>
  struct can_apply:std::false_type{};
  template<template<class...>class Z, class...Ts>
  struct can_apply<Z, void_t<Z<Ts...>>, Ts...>:std::true_type{};
}
template<template<class...>class Z, class...Ts>
using can_apply = details::can_apply<Z, void, Ts...>;

template<class From, class To>
using can_safely_convert = can_apply< safe_convert_result_t, From, To >;

Then can_safely_convert should be true if and only if there is no narrowing conversion going from From to To.

This uses the C++ standard definition of narrowing conversion.

The trick is that C++ forbids {} based initialization to be a narrowing conversion. We use SFINAE to detect if such conversion is legal, and return a truthy type if it is.

Live example.

The standard definition of a narrowing conversion may or may not exactly match what you want. But it is a good place to start.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524