Skip to content

Commit 65591ca

Browse files
committed
[safe_op] Add Safe::cast<T>
1 parent 98e63e4 commit 65591ca

File tree

2 files changed

+212
-0
lines changed

2 files changed

+212
-0
lines changed

src/safe_op.hpp

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,16 @@
2929

3030
#include <limits>
3131
#include <stdexcept>
32+
#include <type_traits>
3233

3334
#ifdef _MSC_VER
3435
#include <Intsafe.h>
36+
37+
// MSVC is stupid and pollutes the global namespace with max() and min() macros
38+
// that break std::numeric_limits<T>::max() and min()
39+
#undef max
40+
#undef min
41+
3542
#endif
3643

3744
/*!
@@ -331,4 +338,117 @@ namespace Safe
331338
return num < 0 ? -num : num;
332339
}
333340

341+
namespace Internal
342+
{
343+
// metafunction to determine whether the integral type `from_t` can be safely converted to the type `to_t`
344+
// without causing over or underflows.
345+
template <typename from_t, typename to_t, typename = void>
346+
struct is_safely_convertible : std::false_type
347+
{
348+
// clang-format off
349+
static_assert(std::is_integral<from_t>::value && std::is_integral<to_t>::value,
350+
"from_t and to_t must both be integral types");
351+
// clang-format on
352+
};
353+
354+
// overload of is_safely_convertible for `from_t` being safely convertible to `to_t`
355+
template <typename from_t, typename to_t>
356+
struct is_safely_convertible<
357+
from_t, to_t,
358+
typename std::enable_if<((std::numeric_limits<from_t>::max() <= std::numeric_limits<to_t>::max()) &&
359+
(std::numeric_limits<from_t>::min() >= std::numeric_limits<to_t>::min()))>::type>
360+
: std::true_type
361+
{
362+
// clang-format off
363+
static_assert(std::is_integral<from_t>::value && std::is_integral<to_t>::value,
364+
"from_t and to_t must both be integral types");
365+
// clang-format on
366+
};
367+
368+
template <typename T, typename U, typename = void>
369+
struct have_same_signedness : std::false_type
370+
{
371+
// clang-format off
372+
static_assert(std::is_integral<T>::value && std::is_integral<U>::value,
373+
"T and U must both be integral types");
374+
// clang-format on
375+
};
376+
377+
// SFINAE overload for (T signed and U signed) or (T unsigned and U unsigned)
378+
template <typename T, typename U>
379+
struct have_same_signedness<T, U,
380+
typename std::enable_if<std::is_signed<T>::value == std::is_signed<U>::value>::type>
381+
: std::true_type
382+
{
383+
// clang-format off
384+
static_assert(std::is_integral<T>::value && std::is_integral<U>::value,
385+
"T and U must both be integral types");
386+
// clang-format on
387+
};
388+
389+
} // namespace Internal
390+
391+
#ifdef PARSED_BY_DOXYGEN
392+
/// Convert a value of type U to type T without causing over- or underflows.
393+
///
394+
/// @throw std::overflow_error When `value` is outside the representable range of T
395+
template <typename T, typename U>
396+
constexpr T cast(U value)
397+
{
398+
}
399+
#else
400+
// trivial version: T can represent all values that U can
401+
template <typename T, typename U>
402+
constexpr typename std::enable_if<Internal::is_safely_convertible<U, T>::value, T>::type cast(U value) noexcept
403+
{
404+
return static_cast<T>(value);
405+
}
406+
407+
// T cannot represent all values that U can,
408+
// but T and U are either both signed or unsigned
409+
// => can compare them without any issues
410+
template <typename T, typename U>
411+
constexpr typename std::enable_if<
412+
(!Internal::is_safely_convertible<U, T>::value) && Internal::have_same_signedness<T, U>::value, T>::type
413+
cast(U value)
414+
{
415+
return (value <= std::numeric_limits<T>::max()) && (value >= std::numeric_limits<T>::min())
416+
? static_cast<T>(value)
417+
: throw std::overflow_error("Cannot convert number without over or underflow");
418+
}
419+
420+
// - T cannot represent all values that U can,
421+
// - T is signed, U is unsigned
422+
// => must cast them compare them without any issues
423+
template <typename T, typename U>
424+
constexpr typename std::enable_if<(!Internal::is_safely_convertible<U, T>::value) && std::is_signed<T>::value &&
425+
std::is_unsigned<U>::value,
426+
T>::type
427+
cast(U value)
428+
{
429+
static_assert(std::numeric_limits<T>::max() < std::numeric_limits<U>::max(),
430+
"maximum value of T must be smaller than the maximum value of U");
431+
// U unsigned, T signed => T_MAX < U_MAX
432+
return (value <= static_cast<U>(std::numeric_limits<T>::max()))
433+
? static_cast<T>(value)
434+
: throw std::overflow_error("Cannot convert number without over or underflow");
435+
}
436+
437+
// - T cannot represent all values that U can,
438+
// - T is unsigned, U is signed
439+
// => must cast them compare them without any issues
440+
template <typename T, typename U>
441+
constexpr typename std::enable_if<(!Internal::is_safely_convertible<U, T>::value) && std::is_unsigned<T>::value &&
442+
std::is_signed<U>::value,
443+
T>::type
444+
cast(U value)
445+
{
446+
// U signed, T unsigned => T_MAX < U_MAX
447+
return (value <= std::numeric_limits<T>::max()) && (value >= std::numeric_limits<T>::min())
448+
? static_cast<T>(value)
449+
: throw std::overflow_error("Cannot convert number without over or underflow");
450+
}
451+
452+
#endif // PARSED_BY_DOXYGEN
453+
334454
} // namespace Safe

unitTests/test_safe_op.cpp

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,95 @@ TEST(safeAbs, checkValues)
190190
}
191191
ASSERT_EQ(Safe::abs(std::numeric_limits<int>::min()), std::numeric_limits<int>::max());
192192
}
193+
194+
//
195+
// sanity checks of is_safely_convertible
196+
//
197+
static_assert(si::is_safely_convertible<uint8_t, uint16_t>::value, "uint8_t must be always convertible to uint16_t");
198+
static_assert(!si::is_safely_convertible<uint16_t, uint8_t>::value, "uint16_t must not always convertible to uint8_t");
199+
200+
static_assert(si::is_safely_convertible<uint8_t, int16_t>::value, "uint8_t must be always convertible to int16_t");
201+
static_assert(!si::is_safely_convertible<int16_t, uint8_t>::value, "int16_t must not always be convertible to uint8_t");
202+
203+
//
204+
// sanity checks for have_same_signedness
205+
//
206+
static_assert(si::have_same_signedness<uint16_t, uint8_t>::value, "uint8_t must have the same signedness as uint16_t");
207+
static_assert(!si::have_same_signedness<int16_t, uint8_t>::value,
208+
"uint8_t must have a different signedness as int16_t");
209+
210+
//
211+
// sanity checks for Safe::cast<>
212+
//
213+
static_assert(std::is_same<decltype(Safe::cast<int>(static_cast<short>(8))), int>::value,
214+
"Return value of Safe::cast<int>(short) must be int");
215+
static_assert(std::is_same<decltype(Safe::cast<int>(8ull)), int>::value,
216+
"Return value of Safe::cast<int>(unsigned long long) must be int");
217+
218+
TEST(SafeCast, TriviallyConvertible)
219+
{
220+
ASSERT_EQ(Safe::cast<int>(static_cast<short>(5)), 5);
221+
}
222+
223+
//
224+
// Test Safe::cast to a signed integer
225+
//
226+
template <typename T>
227+
struct SafeCastToInt16 : public ::testing::Test
228+
{
229+
};
230+
231+
using BiggerRangeThanInt16 = ::testing::Types<uint16_t, int32_t, uint32_t, int64_t, uint64_t>;
232+
233+
TYPED_TEST_CASE(SafeCastToInt16, BiggerRangeThanInt16);
234+
235+
TYPED_TEST(SafeCastToInt16, ThrowsForTooLargeValue)
236+
{
237+
ASSERT_THROW(Safe::cast<int16_t>(static_cast<TypeParam>(std::numeric_limits<int16_t>::max()) + 1),
238+
std::overflow_error);
239+
}
240+
241+
TYPED_TEST(SafeCastToInt16, ThrowsForTooSmallValue)
242+
{
243+
if (std::is_signed<TypeParam>::value) {
244+
ASSERT_THROW(Safe::cast<int16_t>(static_cast<TypeParam>(std::numeric_limits<int16_t>::min()) - 1),
245+
std::overflow_error);
246+
}
247+
}
248+
249+
TYPED_TEST(SafeCastToInt16, DoesNotThrowForRepresentableValue)
250+
{
251+
constexpr TypeParam test_value = std::numeric_limits<int16_t>::max() - 1;
252+
ASSERT_EQ(Safe::cast<int16_t>(test_value), test_value);
253+
}
254+
255+
//
256+
// Test Safe::cast to an unsigned integer
257+
//
258+
template <typename T>
259+
struct SafeCastToUInt32 : public ::testing::Test
260+
{
261+
};
262+
263+
using BiggerRangeThanUInt32 = ::testing::Types<int64_t, uint64_t>;
264+
265+
TYPED_TEST_CASE(SafeCastToUInt32, BiggerRangeThanUInt32);
266+
267+
TYPED_TEST(SafeCastToUInt32, ThrowsForTooLargeValue)
268+
{
269+
ASSERT_THROW(Safe::cast<uint32_t>(static_cast<TypeParam>(std::numeric_limits<uint32_t>::max()) + 1),
270+
std::overflow_error);
271+
}
272+
273+
TYPED_TEST(SafeCastToUInt32, DoesNotThrowForRepresentableValue)
274+
{
275+
constexpr TypeParam test_value = std::numeric_limits<uint32_t>::max() - 1;
276+
ASSERT_EQ(Safe::cast<uint32_t>(test_value), test_value);
277+
}
278+
279+
TYPED_TEST(SafeCastToUInt32, ThrowsForTooSmallValue)
280+
{
281+
if (std::is_signed<TypeParam>::value) {
282+
ASSERT_THROW(Safe::cast<uint32_t>(static_cast<TypeParam>(-1)), std::overflow_error);
283+
}
284+
}

0 commit comments

Comments
 (0)