Skip to content

Commit 596bbbb

Browse files
committed
[safe_op] Add Safe::cast<T>
1 parent 04d770a commit 596bbbb

File tree

2 files changed

+192
-0
lines changed

2 files changed

+192
-0
lines changed

src/safe_op.hpp

Lines changed: 100 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,103 @@ namespace Safe
331332
return num < 0 ? -num : num;
332333
}
333334

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