Skip to content

Commit 2b37528

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

File tree

2 files changed

+206
-0
lines changed

2 files changed

+206
-0
lines changed

src/safe_op.hpp

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

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

3334
#ifdef _MSC_VER
3435
#include <Intsafe.h>
@@ -331,4 +332,117 @@ namespace Safe
331332
return num < 0 ? -num : num;
332333
}
333334

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