Skip to content

Commit

Permalink
[safe_op] Add Safe::cast<T>
Browse files Browse the repository at this point in the history
  • Loading branch information
D4N committed Aug 3, 2019
1 parent 98e63e4 commit 65591ca
Show file tree
Hide file tree
Showing 2 changed files with 212 additions and 0 deletions.
120 changes: 120 additions & 0 deletions src/safe_op.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,16 @@

#include <limits>
#include <stdexcept>
#include <type_traits>

#ifdef _MSC_VER
#include <Intsafe.h>

// MSVC is stupid and pollutes the global namespace with max() and min() macros
// that break std::numeric_limits<T>::max() and min()
#undef max
#undef min

#endif

/*!
Expand Down Expand Up @@ -331,4 +338,117 @@ namespace Safe
return num < 0 ? -num : num;
}

namespace Internal
{
// metafunction to determine whether the integral type `from_t` can be safely converted to the type `to_t`
// without causing over or underflows.
template <typename from_t, typename to_t, typename = void>
struct is_safely_convertible : std::false_type
{
// clang-format off
static_assert(std::is_integral<from_t>::value && std::is_integral<to_t>::value,
"from_t and to_t must both be integral types");
// clang-format on
};

// overload of is_safely_convertible for `from_t` being safely convertible to `to_t`
template <typename from_t, typename to_t>
struct is_safely_convertible<
from_t, to_t,
typename std::enable_if<((std::numeric_limits<from_t>::max() <= std::numeric_limits<to_t>::max()) &&
(std::numeric_limits<from_t>::min() >= std::numeric_limits<to_t>::min()))>::type>
: std::true_type
{
// clang-format off
static_assert(std::is_integral<from_t>::value && std::is_integral<to_t>::value,
"from_t and to_t must both be integral types");
// clang-format on
};

template <typename T, typename U, typename = void>
struct have_same_signedness : std::false_type
{
// clang-format off
static_assert(std::is_integral<T>::value && std::is_integral<U>::value,
"T and U must both be integral types");
// clang-format on
};

// SFINAE overload for (T signed and U signed) or (T unsigned and U unsigned)
template <typename T, typename U>
struct have_same_signedness<T, U,
typename std::enable_if<std::is_signed<T>::value == std::is_signed<U>::value>::type>
: std::true_type
{
// clang-format off
static_assert(std::is_integral<T>::value && std::is_integral<U>::value,
"T and U must both be integral types");
// clang-format on
};

} // namespace Internal

#ifdef PARSED_BY_DOXYGEN
/// Convert a value of type U to type T without causing over- or underflows.
///
/// @throw std::overflow_error When `value` is outside the representable range of T
template <typename T, typename U>
constexpr T cast(U value)
{
}
#else
// trivial version: T can represent all values that U can
template <typename T, typename U>
constexpr typename std::enable_if<Internal::is_safely_convertible<U, T>::value, T>::type cast(U value) noexcept
{
return static_cast<T>(value);
}

// T cannot represent all values that U can,
// but T and U are either both signed or unsigned
// => can compare them without any issues
template <typename T, typename U>
constexpr typename std::enable_if<
(!Internal::is_safely_convertible<U, T>::value) && Internal::have_same_signedness<T, U>::value, T>::type
cast(U value)
{
return (value <= std::numeric_limits<T>::max()) && (value >= std::numeric_limits<T>::min())
? static_cast<T>(value)
: throw std::overflow_error("Cannot convert number without over or underflow");
}

// - T cannot represent all values that U can,
// - T is signed, U is unsigned
// => must cast them compare them without any issues
template <typename T, typename U>
constexpr typename std::enable_if<(!Internal::is_safely_convertible<U, T>::value) && std::is_signed<T>::value &&
std::is_unsigned<U>::value,
T>::type
cast(U value)
{
static_assert(std::numeric_limits<T>::max() < std::numeric_limits<U>::max(),
"maximum value of T must be smaller than the maximum value of U");
// U unsigned, T signed => T_MAX < U_MAX
return (value <= static_cast<U>(std::numeric_limits<T>::max()))
? static_cast<T>(value)
: throw std::overflow_error("Cannot convert number without over or underflow");
}

// - T cannot represent all values that U can,
// - T is unsigned, U is signed
// => must cast them compare them without any issues
template <typename T, typename U>
constexpr typename std::enable_if<(!Internal::is_safely_convertible<U, T>::value) && std::is_unsigned<T>::value &&
std::is_signed<U>::value,
T>::type
cast(U value)
{
// U signed, T unsigned => T_MAX < U_MAX
return (value <= std::numeric_limits<T>::max()) && (value >= std::numeric_limits<T>::min())
? static_cast<T>(value)
: throw std::overflow_error("Cannot convert number without over or underflow");
}

#endif // PARSED_BY_DOXYGEN

} // namespace Safe
92 changes: 92 additions & 0 deletions unitTests/test_safe_op.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,95 @@ TEST(safeAbs, checkValues)
}
ASSERT_EQ(Safe::abs(std::numeric_limits<int>::min()), std::numeric_limits<int>::max());
}

//
// sanity checks of is_safely_convertible
//
static_assert(si::is_safely_convertible<uint8_t, uint16_t>::value, "uint8_t must be always convertible to uint16_t");
static_assert(!si::is_safely_convertible<uint16_t, uint8_t>::value, "uint16_t must not always convertible to uint8_t");

static_assert(si::is_safely_convertible<uint8_t, int16_t>::value, "uint8_t must be always convertible to int16_t");
static_assert(!si::is_safely_convertible<int16_t, uint8_t>::value, "int16_t must not always be convertible to uint8_t");

//
// sanity checks for have_same_signedness
//
static_assert(si::have_same_signedness<uint16_t, uint8_t>::value, "uint8_t must have the same signedness as uint16_t");
static_assert(!si::have_same_signedness<int16_t, uint8_t>::value,
"uint8_t must have a different signedness as int16_t");

//
// sanity checks for Safe::cast<>
//
static_assert(std::is_same<decltype(Safe::cast<int>(static_cast<short>(8))), int>::value,
"Return value of Safe::cast<int>(short) must be int");
static_assert(std::is_same<decltype(Safe::cast<int>(8ull)), int>::value,
"Return value of Safe::cast<int>(unsigned long long) must be int");

TEST(SafeCast, TriviallyConvertible)
{
ASSERT_EQ(Safe::cast<int>(static_cast<short>(5)), 5);
}

//
// Test Safe::cast to a signed integer
//
template <typename T>
struct SafeCastToInt16 : public ::testing::Test
{
};

using BiggerRangeThanInt16 = ::testing::Types<uint16_t, int32_t, uint32_t, int64_t, uint64_t>;

TYPED_TEST_CASE(SafeCastToInt16, BiggerRangeThanInt16);

TYPED_TEST(SafeCastToInt16, ThrowsForTooLargeValue)
{
ASSERT_THROW(Safe::cast<int16_t>(static_cast<TypeParam>(std::numeric_limits<int16_t>::max()) + 1),
std::overflow_error);
}

TYPED_TEST(SafeCastToInt16, ThrowsForTooSmallValue)
{
if (std::is_signed<TypeParam>::value) {
ASSERT_THROW(Safe::cast<int16_t>(static_cast<TypeParam>(std::numeric_limits<int16_t>::min()) - 1),
std::overflow_error);
}
}

TYPED_TEST(SafeCastToInt16, DoesNotThrowForRepresentableValue)
{
constexpr TypeParam test_value = std::numeric_limits<int16_t>::max() - 1;
ASSERT_EQ(Safe::cast<int16_t>(test_value), test_value);
}

//
// Test Safe::cast to an unsigned integer
//
template <typename T>
struct SafeCastToUInt32 : public ::testing::Test
{
};

using BiggerRangeThanUInt32 = ::testing::Types<int64_t, uint64_t>;

TYPED_TEST_CASE(SafeCastToUInt32, BiggerRangeThanUInt32);

TYPED_TEST(SafeCastToUInt32, ThrowsForTooLargeValue)
{
ASSERT_THROW(Safe::cast<uint32_t>(static_cast<TypeParam>(std::numeric_limits<uint32_t>::max()) + 1),
std::overflow_error);
}

TYPED_TEST(SafeCastToUInt32, DoesNotThrowForRepresentableValue)
{
constexpr TypeParam test_value = std::numeric_limits<uint32_t>::max() - 1;
ASSERT_EQ(Safe::cast<uint32_t>(test_value), test_value);
}

TYPED_TEST(SafeCastToUInt32, ThrowsForTooSmallValue)
{
if (std::is_signed<TypeParam>::value) {
ASSERT_THROW(Safe::cast<uint32_t>(static_cast<TypeParam>(-1)), std::overflow_error);
}
}

0 comments on commit 65591ca

Please sign in to comment.