From 65591ca5c19b2ec76070bd844c7033e327de09e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20=C4=8Cerm=C3=A1k?= Date: Tue, 21 May 2019 23:42:54 -0400 Subject: [PATCH] [safe_op] Add Safe::cast --- src/safe_op.hpp | 120 +++++++++++++++++++++++++++++++++++++ unitTests/test_safe_op.cpp | 92 ++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+) diff --git a/src/safe_op.hpp b/src/safe_op.hpp index a89779c4fa..314bebe8e5 100644 --- a/src/safe_op.hpp +++ b/src/safe_op.hpp @@ -29,9 +29,16 @@ #include #include +#include #ifdef _MSC_VER #include + +// MSVC is stupid and pollutes the global namespace with max() and min() macros +// that break std::numeric_limits::max() and min() +#undef max +#undef min + #endif /*! @@ -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 + struct is_safely_convertible : std::false_type + { + // clang-format off + static_assert(std::is_integral::value && std::is_integral::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 + struct is_safely_convertible< + from_t, to_t, + typename std::enable_if<((std::numeric_limits::max() <= std::numeric_limits::max()) && + (std::numeric_limits::min() >= std::numeric_limits::min()))>::type> + : std::true_type + { + // clang-format off + static_assert(std::is_integral::value && std::is_integral::value, + "from_t and to_t must both be integral types"); + // clang-format on + }; + + template + struct have_same_signedness : std::false_type + { + // clang-format off + static_assert(std::is_integral::value && std::is_integral::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 + struct have_same_signedness::value == std::is_signed::value>::type> + : std::true_type + { + // clang-format off + static_assert(std::is_integral::value && std::is_integral::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 + constexpr T cast(U value) + { + } +#else + // trivial version: T can represent all values that U can + template + constexpr typename std::enable_if::value, T>::type cast(U value) noexcept + { + return static_cast(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 + constexpr typename std::enable_if< + (!Internal::is_safely_convertible::value) && Internal::have_same_signedness::value, T>::type + cast(U value) + { + return (value <= std::numeric_limits::max()) && (value >= std::numeric_limits::min()) + ? static_cast(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 + constexpr typename std::enable_if<(!Internal::is_safely_convertible::value) && std::is_signed::value && + std::is_unsigned::value, + T>::type + cast(U value) + { + static_assert(std::numeric_limits::max() < std::numeric_limits::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(std::numeric_limits::max())) + ? static_cast(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 + constexpr typename std::enable_if<(!Internal::is_safely_convertible::value) && std::is_unsigned::value && + std::is_signed::value, + T>::type + cast(U value) + { + // U signed, T unsigned => T_MAX < U_MAX + return (value <= std::numeric_limits::max()) && (value >= std::numeric_limits::min()) + ? static_cast(value) + : throw std::overflow_error("Cannot convert number without over or underflow"); + } + +#endif // PARSED_BY_DOXYGEN + } // namespace Safe diff --git a/unitTests/test_safe_op.cpp b/unitTests/test_safe_op.cpp index 7cd5f8e390..0542d11bd5 100644 --- a/unitTests/test_safe_op.cpp +++ b/unitTests/test_safe_op.cpp @@ -190,3 +190,95 @@ TEST(safeAbs, checkValues) } ASSERT_EQ(Safe::abs(std::numeric_limits::min()), std::numeric_limits::max()); } + +// +// sanity checks of is_safely_convertible +// +static_assert(si::is_safely_convertible::value, "uint8_t must be always convertible to uint16_t"); +static_assert(!si::is_safely_convertible::value, "uint16_t must not always convertible to uint8_t"); + +static_assert(si::is_safely_convertible::value, "uint8_t must be always convertible to int16_t"); +static_assert(!si::is_safely_convertible::value, "int16_t must not always be convertible to uint8_t"); + +// +// sanity checks for have_same_signedness +// +static_assert(si::have_same_signedness::value, "uint8_t must have the same signedness as uint16_t"); +static_assert(!si::have_same_signedness::value, + "uint8_t must have a different signedness as int16_t"); + +// +// sanity checks for Safe::cast<> +// +static_assert(std::is_same(static_cast(8))), int>::value, + "Return value of Safe::cast(short) must be int"); +static_assert(std::is_same(8ull)), int>::value, + "Return value of Safe::cast(unsigned long long) must be int"); + +TEST(SafeCast, TriviallyConvertible) +{ + ASSERT_EQ(Safe::cast(static_cast(5)), 5); +} + +// +// Test Safe::cast to a signed integer +// +template +struct SafeCastToInt16 : public ::testing::Test +{ +}; + +using BiggerRangeThanInt16 = ::testing::Types; + +TYPED_TEST_CASE(SafeCastToInt16, BiggerRangeThanInt16); + +TYPED_TEST(SafeCastToInt16, ThrowsForTooLargeValue) +{ + ASSERT_THROW(Safe::cast(static_cast(std::numeric_limits::max()) + 1), + std::overflow_error); +} + +TYPED_TEST(SafeCastToInt16, ThrowsForTooSmallValue) +{ + if (std::is_signed::value) { + ASSERT_THROW(Safe::cast(static_cast(std::numeric_limits::min()) - 1), + std::overflow_error); + } +} + +TYPED_TEST(SafeCastToInt16, DoesNotThrowForRepresentableValue) +{ + constexpr TypeParam test_value = std::numeric_limits::max() - 1; + ASSERT_EQ(Safe::cast(test_value), test_value); +} + +// +// Test Safe::cast to an unsigned integer +// +template +struct SafeCastToUInt32 : public ::testing::Test +{ +}; + +using BiggerRangeThanUInt32 = ::testing::Types; + +TYPED_TEST_CASE(SafeCastToUInt32, BiggerRangeThanUInt32); + +TYPED_TEST(SafeCastToUInt32, ThrowsForTooLargeValue) +{ + ASSERT_THROW(Safe::cast(static_cast(std::numeric_limits::max()) + 1), + std::overflow_error); +} + +TYPED_TEST(SafeCastToUInt32, DoesNotThrowForRepresentableValue) +{ + constexpr TypeParam test_value = std::numeric_limits::max() - 1; + ASSERT_EQ(Safe::cast(test_value), test_value); +} + +TYPED_TEST(SafeCastToUInt32, ThrowsForTooSmallValue) +{ + if (std::is_signed::value) { + ASSERT_THROW(Safe::cast(static_cast(-1)), std::overflow_error); + } +}