Skip to content

Instantly share code, notes, and snippets.

@jharmer95
Last active February 13, 2023 17:38
Show Gist options
  • Save jharmer95/c609d71f4caf087743483b7a2d8b797d to your computer and use it in GitHub Desktop.
Save jharmer95/c609d71f4caf087743483b7a2d8b797d to your computer and use it in GitHub Desktop.
Safer, more descriptive casts for C++
#ifndef BETTER_CASTS_HPP
#define BETTER_CASTS_HPP
#include <limits>
#include <stdexcept>
#include <type_traits>
#include <utility>
#if __cplusplus >= 201703L
# define INLINE_CONSTEXPR inline constexpr
#else
# define INLINE_CONSTEXPR constexpr
#endif
namespace casts
{
// By default, the checked version of a cast is used in DEBUG builds
// Can always use the explicit checked or unchecked version
#if defined(ALWAYS_CHECK_CASTS) || (!defined(NEVER_CHECK_CASTS) && !defined(NDEBUG))
INLINE_CONSTEXPR bool CHECK_CASTS = true;
#else
INLINE_CONSTEXPR bool CHECK_CASTS = false;
#endif
enum class cast_type
{
enum_cast,
float_cast,
narrow_cast,
sign_cast,
};
template<cast_type Cast>
class cast_error : public std::runtime_error
{
public:
explicit cast_error(const std::string& mesg) : std::runtime_error(mesg) {}
explicit cast_error(const char* mesg) : std::runtime_error(mesg) {}
};
using enum_cast_error = cast_error<cast_type::enum_cast>;
using float_cast_error = cast_error<cast_type::float_cast>;
using narrow_cast_error = cast_error<cast_type::narrow_cast>;
using sign_cast_error = cast_error<cast_type::sign_cast>;
namespace detail
{
template<typename T, typename U>
INLINE_CONSTEXPR bool smaller_size = sizeof(T) < sizeof(U);
template<typename T, typename U>
INLINE_CONSTEXPR bool same_size = sizeof(T) == sizeof(U);
template<typename T, typename U>
INLINE_CONSTEXPR bool larger_size = sizeof(T) > sizeof(U);
template<typename T, typename U>
INLINE_CONSTEXPR bool same_sign = std::is_signed<T>::value == std::is_signed<U>::value;
template<typename T, typename U>
INLINE_CONSTEXPR bool both_int = std::is_integral<T>::value && std::is_integral<U>::value;
template<typename T, typename U>
INLINE_CONSTEXPR bool both_float =
std::is_floating_point<T>::value && std::is_floating_point<U>::value;
template<typename T, typename U>
INLINE_CONSTEXPR bool same_arithmetic = both_int<T, U> || both_float<T, U>;
template<typename T, typename = std::enable_if_t<std::is_arithmetic<T>::value>>
constexpr T abs(T t) noexcept
{
return t < 0 ? -t : t;
}
template<typename T, typename U, typename = std::enable_if_t<std::is_arithmetic<T>::value && std::is_floating_point<U>::value>>
constexpr T round(U u) noexcept
{
if (u >= 0.0)
{
return abs(static_cast<std::intmax_t>(u) - u) >= abs(static_cast<std::intmax_t>(u) - u + 1) ?
static_cast<T>(u) + 1 :
static_cast<T>(u);
}
return abs(static_cast<std::intmax_t>(u) - u) >= abs(static_cast<std::intmax_t>(u) - u - 1) ?
static_cast<T>(u) - 1 :
static_cast<T>(u);
}
template<typename T, typename U, typename = std::enable_if_t<std::is_arithmetic<T>::value && std::is_floating_point<U>::value>>
constexpr T floor(U u) noexcept
{
return u < 0.0 ? static_cast<T>(u) - 1 : static_cast<T>(u);
}
template<typename T, typename U, typename = std::enable_if_t<std::is_arithmetic<T>::value && std::is_floating_point<U>::value>>
constexpr T ceiling(U u) noexcept
{
return u < 0.0 ? static_cast<T>(u) : static_cast<T>(u) + 1;
}
} // namespace detail
template<typename T, typename U>
struct is_enum_castable :
std::integral_constant<bool,
((std::is_enum<T>::value || std::is_enum<U>::value)
&& (detail::same_size<T, U> || detail::larger_size<T, U>)&&detail::same_sign<
std::underlying_type_t<T>, std::underlying_type_t<U>>)>
{
// TODO: Can check if valid enumerator? (magic_enum)
};
template<typename T, typename U>
INLINE_CONSTEXPR bool is_enum_castable_v = is_enum_castable<T, U>::value;
template<typename T, typename U>
[[nodiscard]] constexpr T enum_cast_unchecked(U&& u) noexcept
{
static_assert(is_enum_castable_v<T, U>, "U does not meet the requirements to be casted to a T");
return static_cast<T>(std::forward<U>(u));
}
template<typename T, typename U, std::enable_if_t<sizeof(T) < sizeof(U), bool> = true>
[[nodiscard]] constexpr T enum_cast_checked(U u) noexcept(false)
{
static_assert(is_enum_castable_v<T, U>, "U does not meet the requirements to be casted to a T");
// TODO: Can check for valid numerator?
if (u > static_cast<U>((std::numeric_limits<T>::max)()))
{
throw enum_cast_error{ "enum_cast failed: input exceeded max value for output type" };
}
if (u < static_cast<U>((std::numeric_limits<T>::min)()))
{
throw enum_cast_error{ "enum_cast failed: input exceeded min value for output type" };
}
return static_cast<T>(u);
}
template<typename T, typename U, std::enable_if_t<sizeof(T) >= sizeof(U), bool> = true>
[[nodiscard]] constexpr T enum_cast_checked(U u) noexcept
{
static_assert(is_enum_castable_v<T, U>, "U does not meet the requirements to be casted to a T");
return static_cast<T>(u);
}
template<typename T, typename U>
[[nodiscard]] constexpr std::enable_if_t<CHECK_CASTS, T> enum_cast(U&& u) noexcept(
sizeof(T) >= sizeof(U))
{
static_assert(is_enum_castable_v<T, std::remove_cv_t<std::remove_reference_t<U>>>,
"U does not meet the requirements to be casted to a T");
return enum_cast_checked<T>(std::forward<U>(u));
}
template<typename T, typename U>
[[nodiscard]] constexpr std::enable_if_t<!CHECK_CASTS, T> enum_cast(U&& u) noexcept
{
static_assert(is_enum_castable_v<T, std::remove_cv_t<std::remove_reference_t<U>>>,
"U does not meet the requirements to be casted to a T");
return enum_cast_unchecked<T>(std::forward<U>(u));
}
template<typename T, typename U>
struct is_float_castable :
std::integral_constant<bool,
(std::is_integral<T>::value && !std::is_same<T, bool>::value
&& std::is_floating_point<U>::value)>
{
};
template<typename T, typename U>
INLINE_CONSTEXPR bool is_float_castable_v = is_float_castable<T, U>::value;
enum class float_cast_op
{
truncate,
round,
floor,
ceiling,
};
template<typename T, typename U>
[[nodiscard]] constexpr T float_cast_unchecked(
U&& u, float_cast_op op = float_cast_op::truncate) noexcept
{
static_assert(is_float_castable_v<T, std::remove_cv_t<std::remove_reference_t<U>>>,
"U does not meet the requirements to be casted to a T");
switch (op)
{
case float_cast_op::truncate:
return static_cast<T>(std::forward<U>(u));
case float_cast_op::round:
return detail::round<T>(std::forward<U>(u));
case float_cast_op::floor:
return detail::floor<T>(std::forward<U>(u));
case float_cast_op::ceiling:
return detail::ceiling<T>(std::forward<U>(u));
}
}
template<typename T, typename U>
[[nodiscard]] constexpr T float_cast_checked(
U u, float_cast_op op = float_cast_op::truncate) noexcept(false)
{
static_assert(is_float_castable_v<T, std::remove_cv_t<std::remove_reference_t<U>>>,
"U does not meet the requirements to be casted to a T");
switch (op)
{
case float_cast_op::truncate:
if (u - 1.0 >= static_cast<U>((std::numeric_limits<T>::max)()))
{
throw float_cast_error{"float_cast (truncate) failed: input exceeded max value for output type"};
}
if (u + 1.0 <= static_cast<U>((std::numeric_limits<T>::min)()))
{
throw float_cast_error{"float_cast (truncate) failed: input exceeded min value for output type"};
}
return static_cast<T>(std::forward<U>(u));
case float_cast_op::round:
if (u > 0.0 && u - 0.5 >= static_cast<U>((std::numeric_limits<T>::max)()))
{
throw float_cast_error{"float_cast (round) failed: input exceeded max value for output type"};
}
if (u < 0.0 && u + 0.5 <= static_cast<U>((std::numeric_limits<T>::min)()))
{
throw float_cast_error{"float_cast (round) failed: input exceeded min value for output type"};
}
return detail::round<T>(std::forward<U>(u));
case float_cast_op::floor:
if (u > 0.0 && u - 1.0 >= static_cast<U>((std::numeric_limits<T>::max)()))
{
throw float_cast_error{"float_cast (floor) failed: input exceeded max value for output type"};
}
if (u < 0.0 && u < static_cast<U>((std::numeric_limits<T>::min)()))
{
throw float_cast_error{"float_cast (floor) failed: input exceeded min value for output type"};
}
return detail::floor<T>(std::forward<U>(u));
case float_cast_op::ceiling:
if (u > 0.0 && u > static_cast<U>((std::numeric_limits<T>::max)()))
{
throw float_cast_error{"float_cast (ceiling) failed: input exceeded max value for output type"};
}
if (u < 0.0 && u + 1.0 <= static_cast<U>((std::numeric_limits<T>::min)()))
{
throw float_cast_error{"float_cast (ceiling) failed: input exceeded min value for output type"};
}
return detail::ceiling<T>(std::forward<U>(u));
}
}
template<typename T, typename U>
[[nodiscard]] constexpr std::enable_if_t<CHECK_CASTS, T> float_cast(U&& u, float_cast_op op = float_cast_op::truncate) noexcept
{
static_assert(is_float_castable_v<T, std::remove_cv_t<std::remove_reference_t<U>>>,
"U does not meet the requirements to be casted to a T");
return float_cast_checked<T>(std::forward<U>(u), op);
}
template<typename T, typename U>
[[nodiscard]] constexpr std::enable_if_t<!CHECK_CASTS, T> float_cast(U&& u, float_cast_op op = float_cast_op::truncate) noexcept
{
static_assert(is_float_castable_v<T, std::remove_cv_t<std::remove_reference_t<U>>>,
"U does not meet the requirements to be casted to a T");
return float_cast_unchecked<T>(std::forward<U>(u), op);
}
template<typename T, typename U>
struct is_narrow_castable :
std::integral_constant<bool,
((detail::smaller_size<T, U> || detail::same_size<T, U>) && detail::same_sign<T, U>
&& std::is_arithmetic<T>::value && std::is_arithmetic<U>::value
&& !std::is_same<T, bool>::value && !std::is_same<U, bool>::value
&& detail::same_arithmetic<T, U> && !std::is_enum<T>::value && !std::is_enum<U>::value)>
{
};
template<typename T, typename U>
INLINE_CONSTEXPR bool is_narrow_castable_v = is_narrow_castable<T, U>::value;
template<typename T, typename U>
[[nodiscard]] constexpr T narrow_cast_unchecked(U&& u) noexcept
{
static_assert(is_narrow_castable_v<T, std::remove_cv_t<std::remove_reference_t<U>>>,
"U does not meet the requirements to be casted to a T");
return static_cast<T>(std::forward<U>(u));
}
template<typename T, typename U,
std::enable_if_t<(sizeof(T) < sizeof(U) && std::is_signed<T>::value), bool> = true>
[[nodiscard]] constexpr T narrow_cast_checked(U u) noexcept(false)
{
static_assert(
is_narrow_castable_v<T, U>, "U does not meet the requirements to be casted to a T");
if (u > static_cast<U>((std::numeric_limits<T>::max)()))
{
throw narrow_cast_error{ "narrow_cast failed: input exceeded max value for output type" };
}
if (u < static_cast<U>((std::numeric_limits<T>::min)()))
{
throw narrow_cast_error{ "narrow_cast failed: input exceeded min value for output type" };
}
return static_cast<T>(u);
}
template<typename T, typename U,
std::enable_if_t<(sizeof(T) < sizeof(U) && std::is_unsigned<T>::value), bool> = true>
[[nodiscard]] constexpr T narrow_cast_checked(U u) noexcept(false)
{
static_assert(
is_narrow_castable_v<T, U>, "U does not meet the requirements to be casted to a T");
if (u > static_cast<U>((std::numeric_limits<T>::max)()))
{
throw narrow_cast_error{ "narrow_cast failed: input exceeded max value for output type" };
}
return static_cast<T>(u);
}
template<typename T, typename U, std::enable_if_t<sizeof(T) == sizeof(U), bool> = true>
[[nodiscard]] constexpr T narrow_cast_checked(U u) noexcept
{
static_assert(
is_narrow_castable_v<T, U>, "U does not meet the requirements to be casted to a T");
return static_cast<T>(u);
}
template<typename T, typename U>
[[nodiscard]] constexpr std::enable_if_t<CHECK_CASTS, T> narrow_cast(U&& u) noexcept(
sizeof(T) == sizeof(U))
{
static_assert(is_narrow_castable_v<T, std::remove_cv_t<std::remove_reference_t<U>>>,
"U does not meet the requirements to be casted to a T");
return narrow_cast_checked<T>(std::forward<U>(u));
}
template<typename T, typename U>
[[nodiscard]] constexpr std::enable_if_t<!CHECK_CASTS, T> narrow_cast(U&& u) noexcept
{
static_assert(is_narrow_castable_v<T, std::remove_cv_t<std::remove_reference_t<U>>>,
"U does not meet the requirements to be casted to a T");
return narrow_cast_unchecked<T>(std::forward<U>(u));
}
template<typename T, typename U>
struct is_sign_castable :
std::integral_constant<bool,
(std::is_integral<T>::value && std::is_integral<U>::value
&& (detail::same_size<T, U> || detail::larger_size<T, U>)&&!detail::same_sign<T, U>)>
{
};
template<typename T, typename U>
INLINE_CONSTEXPR bool is_sign_castable_v = is_sign_castable<T, U>::value;
template<typename T, typename U>
[[nodiscard]] constexpr T sign_cast_unchecked(U&& u) noexcept
{
static_assert(is_sign_castable_v<T, U>, "U does not meet the requirements to be casted to a T");
return static_cast<T>(std::forward<U>(u));
}
template<typename T, typename U, std::enable_if_t<std::is_unsigned<T>::value, bool> = true>
[[nodiscard]] constexpr T sign_cast_checked(U u) noexcept(false)
{
static_assert(is_sign_castable_v<T, U>, "U does not meet the requirements to be casted to a T");
if (u < 0)
{
throw sign_cast_error{ "sign failed: cannot cast a negative number to unsigned" };
}
return static_cast<T>(u);
}
template<typename T, typename U,
std::enable_if_t<(std::is_signed<T>::value && sizeof(T) == sizeof(U)), bool> = true>
[[nodiscard]] constexpr T sign_cast_checked(U u) noexcept(false)
{
static_assert(is_sign_castable_v<T, U>, "U does not meet the requirements to be casted to a T");
if (u > static_cast<U>((std::numeric_limits<T>::max)()))
{
throw sign_cast_error{ "sign_cast failed: input exceeded max value for output type" };
}
return static_cast<T>(u);
}
template<typename T, typename U,
std::enable_if_t<(std::is_signed<T>::value && sizeof(T) > sizeof(U)), bool> = true>
[[nodiscard]] constexpr T sign_cast_checked(U u) noexcept
{
static_assert(is_sign_castable_v<T, U>, "U does not meet the requirements to be casted to a T");
return static_cast<T>(u);
}
template<typename T, typename U>
[[nodiscard]] constexpr std::enable_if_t<CHECK_CASTS, T> sign_cast(U&& u) noexcept(
std::is_signed<T>::value && sizeof(T) > sizeof(U))
{
static_assert(is_sign_castable_v<T, std::remove_cv_t<std::remove_reference_t<U>>>,
"U does not meet the requirements to be casted to a T");
return sign_cast_checked<T>(std::forward<U>(u));
}
template<typename T, typename U>
[[nodiscard]] constexpr std::enable_if_t<!CHECK_CASTS, T> sign_cast(U&& u) noexcept
{
static_assert(is_sign_castable_v<T, std::remove_cv_t<std::remove_reference_t<U>>>,
"U does not meet the requirements to be casted to a T");
return sign_cast_unchecked<T>(std::forward<U>(u));
}
} // namespace casts
#endif // BETTER_CASTS_HPP
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment