Last active
February 13, 2023 17:38
-
-
Save jharmer95/c609d71f4caf087743483b7a2d8b797d to your computer and use it in GitHub Desktop.
Safer, more descriptive casts for C++
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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