The rationale behind this code is to implement a runtime-safe number conversions in situations when precision loss is possible, but is not expected. Example: passing a size_t value from a 64bit code to a library (filesystem, database, etc.) which uses 32bit type for size, assuming you will never pass more than 4Gb of data. Safety here means cast result would have an *exactly same* **numeric** (not binary) value (i.e. any rounding, value wrapping, sign re-interpretation, etc. would be treated as casting failure). At the same time, simple impilicit casting for maximum performance is highly desired. This is especially useful for template classes, which usually assumed to have no special treatment to the types they operate on. Since it would be used in many places of my code, I’m wondering if I’ve overlooked something.

Here’s the code (*note that “to”-template argument goes before “from”-argument for automatic argument deduction in real-world usage*):

```
#include <limits>
#include <type_traits>
#include <stdexcept>
#include <typeinfo>
class SafeNumericCast {
protected:
enum class NumberClass {
UNSIGNED_INTEGER, SIGNED_INTEGER, IEEE754
};
protected:
template <typename T> static constexpr NumberClass resolveNumberClass() {
static_assert(std::numeric_limits<T>::radix == 2, "Safe numeric casts can only be performed on binary number formats!");
if constexpr (std::numeric_limits<T>::is_integer) {
if constexpr (!std::is_same<T, bool>::value) { // NOTE Boolean is conceptually not a number (while it is technically backed by one)
return std::numeric_limits<T>::is_signed ? NumberClass::SIGNED_INTEGER : NumberClass::UNSIGNED_INTEGER;
}
} else if constexpr (std::numeric_limits<T>::is_iec559) {
return NumberClass::IEEE754;
}
throw std::logic_error("SafeNumericCast > Unsupported numeric type!");
}
public:
template <typename TTo, typename TFrom> static constexpr bool isSafelyCastable() {
if constexpr (!std::is_same<TTo, TFrom>::value) {
const NumberClass toNumberClass = resolveNumberClass<TTo>();
const NumberClass fromNumberClass = resolveNumberClass<TFrom>();
if constexpr (toNumberClass == NumberClass::UNSIGNED_INTEGER) {
if constexpr (fromNumberClass == NumberClass::UNSIGNED_INTEGER) {
return std::numeric_limits<TTo>::digits >= std::numeric_limits<TFrom>::digits;
}
} else if constexpr (toNumberClass == NumberClass::SIGNED_INTEGER) {
if constexpr ((fromNumberClass == NumberClass::UNSIGNED_INTEGER) || (fromNumberClass == NumberClass::SIGNED_INTEGER)) {
return std::numeric_limits<TTo>::digits >= std::numeric_limits<TFrom>::digits;
}
} else if constexpr (toNumberClass == NumberClass::IEEE754) {
if constexpr ((fromNumberClass == NumberClass::UNSIGNED_INTEGER) || (fromNumberClass == NumberClass::SIGNED_INTEGER) || (fromNumberClass == NumberClass::IEEE754)) {
return std::numeric_limits<TTo>::digits >= std::numeric_limits<TFrom>::digits;
}
}
return false;
}
return true;
}
template <typename TTo, typename TFrom> static constexpr TTo cast(TFrom value) {
static_assert(isSafelyCastable<TTo, TFrom>());
return value;
}
};
class SafeRuntimeNumericCast : public SafeNumericCast {
private:
template <typename TTo, typename TFrom> static constexpr bool isRuntimeCastable(TFrom value, TTo casted) {
static_assert(!SafeNumericCast::isSafelyCastable<TTo, TFrom>());
const NumberClass toNumberClass = resolveNumberClass<TTo>();
const NumberClass fromNumberClass = resolveNumberClass<TFrom>();
if constexpr (toNumberClass == NumberClass::UNSIGNED_INTEGER) {
if constexpr (fromNumberClass == NumberClass::UNSIGNED_INTEGER) {
return value <= std::numeric_limits<TTo>::max();
} else if constexpr (fromNumberClass == NumberClass::SIGNED_INTEGER) {
if (value > 0) {
return value <= std::numeric_limits<TTo>::max();
}
} else if constexpr (fromNumberClass == NumberClass::IEEE754) {
return casted == value;
}
} else if constexpr (toNumberClass == NumberClass::SIGNED_INTEGER) {
if constexpr (fromNumberClass == NumberClass::UNSIGNED_INTEGER) {
return value <= std::numeric_limits<TTo>::max();
} else if constexpr (fromNumberClass == NumberClass::SIGNED_INTEGER) {
return ((value >= std::numeric_limits<TTo>::min()) &&(value <= std::numeric_limits<TTo>::max()));
} else if constexpr (fromNumberClass == NumberClass::IEEE754) {
return casted == value;
}
} else if constexpr (toNumberClass == NumberClass::IEEE754) {
if constexpr (fromNumberClass == NumberClass::UNSIGNED_INTEGER) {
return value <= (1ULL << std::numeric_limits<TTo>::digits); // NOTE Can't do "casted == value" check because of int-> float promotion
} else if constexpr (fromNumberClass == NumberClass::SIGNED_INTEGER) {
return static_cast<TFrom>(casted) == value; // NOTE Presumable faster than doing abs(value)
} else if constexpr (fromNumberClass == NumberClass::IEEE754) {
return (casted == value) || (value != value);
}
}
return false;
}
public:
using SafeNumericCast::isSafelyCastable;
template <typename TTo, typename TFrom> static constexpr bool isSafelyCastable(TFrom value) {
if constexpr (!SafeNumericCast::isSafelyCastable<TTo, TFrom>()) {
return isRuntimeCastable<TTo>(value, static_cast<TTo>(value));
}
return true;
}
template <typename TTo, typename TFrom> static constexpr TTo cast(TFrom value) {
if constexpr (!SafeNumericCast::isSafelyCastable<TTo, TFrom>()) {
TTo casted = static_cast<TTo>(value);
if (isRuntimeCastable<TTo>(value, casted)) {
return casted;
}
throw std::bad_cast();
}
return value;
}
};
```

The usage is simple:

```
SafeNumericCast::cast<uint64_t>(42); // Statically check for possible precision loss
SafeRuntimeNumericCast::cast<float>(1ULL); // Dynamically check for precision loss
SafeNumericCast::isSafelyCastable<uint64_t, uint32_t>(); // Non-throwing static check
SafeRuntimeNumericCast::isSafelyCastable<float>(1ULL); // Non-throwing dynamic check
```

Here are the assumptions the code is based on:

- The code is working only on 10 built-in binary numeric types – this is intended for now
- Any unsigned integer can be exactly represented by another signed or unsigned integer as long as it has enough digit capacity, otherwise a runtime value check against max value is required
- Any signed integer can be exactly represented by another signed integer as long as it has enough digit capacity, otherwise a runtime value check against min and max values is required
- Signed integer can’t be generally represented by an unsigned integer, so a runtime value check is required. We can’t simply compare by value due to sign re-interpretation during signed/unsigned promotion, so we have to separately check for negative sign and positive value against possible max value
- Integers can be represented by an IEEE 754 float as long as it has enough digit capacity, otherwise a runtime value check is required. We can’t simply compare by value due to possible rounding during integer/float promotion, so we have to manually check against maximum representable integer.
- IEEE 754 floats can’t be generally represented by an integer, so we have to check at runtime by simply comparing original and casted values. This should also cover NaN/Infinity/etc cases.
- Any IEEE 754 float can be exactly represented by another IEEE 754 float as long as it has same or bigger size (that is, double simply has more capacity for both mantissa and exponent, thus any float is exactly representable by a double). Otherwise, a simple runtime value comparison is required. The only corner case is NaN and
`std::isnan()`

is, unfortunately is not constexpr, but we can work it around by checking `value != value`

.