Inspired by gsl::narrow_cast, I created my own implementation with the addition of a couple of features:
- a static assert on the types to ensure the cast is actually narrowing (if future changes to the code mean the cast is no longer narrowing, we don't want to still have a
narrow_cast
there) - the version without a runtime check still has an assert, so it's checked in debug mode
#include <cassert>
#include <exception>
#include <iostream>
#include <type_traits>
namespace details {
template <typename T, typename U>
constexpr bool is_same_signedness = std::is_signed<T>::value == std::is_signed<U>::value;
template <typename T, typename U>
constexpr bool can_fully_represent =
std::is_same<T, U>::value ||
( std::is_integral<T>::value && std::is_integral<U>::value &&
( ( std::is_signed<T>::value && sizeof(T) > sizeof(U) ) ||
( is_same_signedness<T, U> && sizeof(T) >= sizeof(U) ) ) ) ||
( std::is_floating_point<T>::value && std::is_floating_point<U>::value && sizeof(T) >= sizeof(U) );
template <typename T, typename U>
constexpr bool static_cast_changes_value(U u) noexcept
{
const auto t = static_cast<T>(u);
// this should catch most cases, but may miss dodgy unsigned to signed conversion or vice-versa
if (static_cast<U>(t) != u)
return true;
if (std::is_signed<T>::value != std::is_signed<U>::value && ((t < T{}) != (u < U{})))
return true;
return false;
}
} // namespace details
// TODO: unchecked cast for types where some loss of precision (and therefore assertion failure) is expected?
template <typename T, typename U>
constexpr T narrow_cast(U&& u) noexcept
{
static_assert(!details::can_fully_represent<T, U>, "we shouldn't be using narrow_cast for casts that aren't actually narrowing");
assert(!details::static_cast_changes_value<T>(u));
return static_cast<T>(std::forward<U>(u));
}
struct narrowing_error : public std::exception {};
template <typename T, typename U>
constexpr T narrow_cast_checked(U u)
{
static_assert(!details::can_fully_represent<T, U>, "we shouldn't be using narrow_cast for casts that aren't actually narrowing");
if (details::static_cast_changes_value<T>(u))
throw narrowing_error();
return static_cast<T>(u);
}
narrow_cast<>()
? Consider that yournarrow_cast<>()
might be used in templates that want to allow safe narrowing of values, but might be instantiated with types such that it doesn't actually narrow. Then the use of that template will result in a compile-time error, unless it does something likeif constexpr (is_narrower<T, U>()) t = narrow_cast<T>(u); else t = u;
\$\endgroup\$ – G. Sliepen Oct 18 '19 at 9:24static_cast
for that situation, but now I'm thinking it might be useful to have amaybe_narrow_cast
to retain the runtime assert. \$\endgroup\$ – John Ilacqua Oct 18 '19 at 9:45