Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Safe::cast<T> #898

Open
wants to merge 1 commit into
base: old-master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

Comment on lines +36 to +41
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is actually already defined in cmake/compilerFlags.cmake. Although it seems it is only defined for MSVC. @D4N maybe you had issues with MinGW ?

#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())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is possible that T_MAX > U_MAX here, because this code could be used for casts like int8_t to uint32_t. I think the code already works correctly, because value will get automatically promoted to type T in the comparison. But I would recommend swapping the order to first check that value isn't negative:

0 <= value && value <= std::numeric_limits<T>::max()

? 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);
}
}