Skip to content

Instantly share code, notes, and snippets.

@imneme
Created October 11, 2025 00:11
Show Gist options
  • Save imneme/8f176e3ba72041b0af3375edfdba474d to your computer and use it in GitHub Desktop.
Save imneme/8f176e3ba72041b0af3375edfdba474d to your computer and use it in GitHub Desktop.
Safe numeric conversions for C++ — comprehensive `narrowing_cast` that throws on overflow, with handy shorthands like `to_int`.
/*
* Safe numeric casts for C++.
*
* - Provides a function template `narrowing_cast<To>(From value)` that safely
* converts from one numeric type to another, throwing std::overflow_error
* if the value cannot be represented in the target type.
*
* - Also provides convenience functions like `to_int16_t(value)`,
* `to_int32_t(value)`, and `to_int64_t(value)` for common conversions.
*
* It's in the `meo` namespace by default, but you can #define
* NARROWING_CAST_NAMESPACE to put it in a different namespace if you want.
*
* Copyright (c) 2025 Melissa O'Neill
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the “Software”),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*
*/
#ifndef NARROWING_CAST_HPP_INCLUDED
#define NARROWING_CAST_HPP_INCLUDED
#include <cstdint>
#include <type_traits>
#include <stdexcept>
#include <limits>
#include <cstddef>
#include <cstdint>
#ifndef NARROWING_CAST_NAMESPACE
namespace meo {
#else
namespace NARROWING_CAST_NAMESPACE {
#endif
// Safe downcast from one integral type to a smaller one.
// If the value cannot fit in the smaller type, throws std::overflow_error.
// Uses C++20 concepts to restrict the types to integral types only.
template <typename To, typename From>
requires std::is_integral_v<To> && std::is_integral_v<From>
To narrowing_cast(From value) {
constexpr auto fromDigits = std::numeric_limits<From>::digits;
constexpr auto toDigits = std::numeric_limits<To>::digits;
constexpr bool sameSignedness =
std::is_signed_v<From> == std::is_signed_v<To>;
constexpr bool unsignedToSigned =
!std::is_signed_v<From> && std::is_signed_v<To>;
constexpr bool signedToUnsigned =
std::is_signed_v<From> && !std::is_signed_v<To>;
// If it's a promotion then it's always safe.
if constexpr (fromDigits <= toDigits
&& (sameSignedness || unsignedToSigned)) {
return static_cast<To>(value);
} else {
// Otherwise, we need to check the range.
if constexpr (signedToUnsigned) {
if (value < 0) {
throw std::overflow_error(
"narrowing_cast: negative value to unsigned");
}
if (static_cast<std::make_unsigned_t<From>>(value)
> std::numeric_limits<To>::max()) {
throw std::overflow_error(
"narrowing_cast: value too large for target type");
}
} else if constexpr (unsignedToSigned) {
if (value > static_cast<std::make_unsigned_t<To>>(
std::numeric_limits<To>::max())) {
throw std::overflow_error(
"narrowing_cast: value too large for target type");
}
} else {
// Both are signed or both are unsigned
if (value < std::numeric_limits<To>::min()
|| value > std::numeric_limits<To>::max()) {
throw std::overflow_error(
"narrowing_cast: value out of range for target type");
}
}
return static_cast<To>(value);
}
}
// We also support narrowing_cast from enum types to integral types
template <typename To, typename From>
requires std::is_integral_v<To> && std::is_enum_v<From>
To narrowing_cast(From value) {
using Underlying = std::underlying_type_t<From>;
return narrowing_cast<To>(static_cast<Underlying>(value));
}
// Possibly unwisely, we support narrowing_cast from floating
// point types to integral types. This is potentially lossy in two ways:
// the fractional part is discarded, and the integral part may be out of
// range for the target type. We assume that the user understands about the
// rounding behavior, but we do check for range errors.
template <typename To, typename From>
requires std::is_integral_v<To> && std::is_floating_point_v<From>
To narrowing_cast(From value) {
if (value < static_cast<From>(std::numeric_limits<To>::min())
|| value > static_cast<From>(std::numeric_limits<To>::max())) {
throw std::overflow_error(
"narrowing_cast: value out of range for target type");
}
return static_cast<To>(value);
}
// We also allow narrowing within floating point types, which can lose
// precision, but we don't check for that. We do check for range errors,
template <typename To, typename From>
requires std::is_floating_point_v<To> && std::is_floating_point_v<From>
To narrowing_cast(From value) {
if constexpr (std::numeric_limits<From>::digits
<= std::numeric_limits<To>::digits) {
// Always safe to go to same or larger size
return static_cast<To>(value);
} else {
// Check for range errors
if (value < static_cast<From>(-std::numeric_limits<To>::max())
|| value > static_cast<From>(std::numeric_limits<To>::max())) {
throw std::overflow_error(
"narrowing_cast: value out of range for target type");
}
return static_cast<To>(value);
}
}
// Finally, we allow narrowing_cast from integral types to floating
// point types. Here we *do* care about loss of precision, because people
// can be surprised when a floating point type can't exactly represent
// all integers in its range. So we check for that too.
template <typename To, typename From>
requires std::is_floating_point_v<To> && std::is_integral_v<From>
To narrowing_cast(From value) {
// Approach: We look at the number of bits in the mantissa
// (including the implicit leading 1 bit). If the integral type
// has more bits than that, we need to check for loss of precision.
constexpr int mantissaBits = std::numeric_limits<To>::digits;
if constexpr (sizeof(From) * 8 - std::is_signed_v<From> > mantissaBits) {
// We have more bits than the mantissa can represent, but we can work
// out what the largest exact integer is that can be represented. It's
// 2^mantissaBits. So we need to check that the value is in the range
// [-2^mantissaBits, 2^mantissaBits].
constexpr From maxExact = From(1) << mantissaBits;
bool failed;
if constexpr (std::is_signed_v<From>) {
// Signed integral type
constexpr From minExact = -maxExact;
failed = (value < minExact || value > maxExact);
} else {
// Unsigned integral type
failed = value > maxExact;
}
if (failed) {
throw std::overflow_error(
"narrowing_cast: integral value too large for exact "
"floating-point representation");
}
}
return static_cast<To>(value);
}
// Convenience wrappers for common cases
// Alas, the best way to do this is with macros.
#define NCAST_DEFINE_TO_TYPE_FUNC_WITH_NAME(type, name) \
template <typename From> \
inline type to_##name(From value) { \
return narrowing_cast<type>(value); \
}
#define NCAST_DEFINE_TO_TYPE_FUNC(type) \
NCAST_DEFINE_TO_TYPE_FUNC_WITH_NAME(type, type)
NCAST_DEFINE_TO_TYPE_FUNC(int8_t)
NCAST_DEFINE_TO_TYPE_FUNC(uint8_t)
NCAST_DEFINE_TO_TYPE_FUNC(int16_t)
NCAST_DEFINE_TO_TYPE_FUNC(uint16_t)
NCAST_DEFINE_TO_TYPE_FUNC(int32_t)
NCAST_DEFINE_TO_TYPE_FUNC(uint32_t)
NCAST_DEFINE_TO_TYPE_FUNC(int64_t)
NCAST_DEFINE_TO_TYPE_FUNC(uint64_t)
NCAST_DEFINE_TO_TYPE_FUNC(size_t)
NCAST_DEFINE_TO_TYPE_FUNC(ptrdiff_t)
// Standard integer types int, short and long and unsigned variants
NCAST_DEFINE_TO_TYPE_FUNC(int)
NCAST_DEFINE_TO_TYPE_FUNC(short)
NCAST_DEFINE_TO_TYPE_FUNC(long)
NCAST_DEFINE_TO_TYPE_FUNC_WITH_NAME(long long, longlong)
NCAST_DEFINE_TO_TYPE_FUNC_WITH_NAME(unsigned int, uint)
NCAST_DEFINE_TO_TYPE_FUNC_WITH_NAME(unsigned short, ushort)
NCAST_DEFINE_TO_TYPE_FUNC_WITH_NAME(unsigned long, ulong)
NCAST_DEFINE_TO_TYPE_FUNC_WITH_NAME(unsigned long long, ulonglong)
// Floating point types
NCAST_DEFINE_TO_TYPE_FUNC(float)
NCAST_DEFINE_TO_TYPE_FUNC(double)
NCAST_DEFINE_TO_TYPE_FUNC_WITH_NAME(long double, longdouble)
#undef NCAST_DEFINE_TO_TYPE_FUNC
#undef NCAST_DEFINE_TO_TYPE_FUNC_WITH_NAME
} // namespace
#ifdef NARROWING_CAST_POLLUTE_GLOBAL_NAMESPACE
#ifdef NARROWING_CAST_NAMESPACE
using namespace NARROWING_CAST_NAMESPACE;
#else
using namespace meo;
#endif
#endif
#endif // NARROWING_CAST_HPP_INCLUDED
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment