Overflow Policies
Description
The library provides multiple overflow handling policies for arithmetic and shift operations. There are two ways to select a policy:
-
Per call site: named free functions (
saturating_add,checked_add, …) and a generic policy-parameterized interface (add<Policy>). -
Per type: a second template parameter on the numeric types themselves, with ready-made aliases such as
sat_u8andstrict_i32. The default arithmetic operators (+,-,*,/,%) and shift operators (<<,>>) follow the type’s policy, which isthrow_exceptionfor the plain aliases (u8,i32,f64, …).
The call-site families apply to the integer types.
The floating-point types f32 and f64 follow IEEE 754 semantics instead: their exceptional results (saturation to infinity, invalid operations, and NaN) are reported as exceptions directly, and the only call-site family provided for them is overflowing_*.
As a type-level policy, floats additionally support saturate, which is raw IEEE 754 arithmetic with no checks at all.
See Floating-Point Types.
The overflow_policy Enum
#include <boost/safe_numbers/overflow_policy.hpp>
namespace boost::safe_numbers {
enum class overflow_policy
{
throw_exception, // Throw an exception on overflow/underflow
saturate, // Clamp to the representable range
overflow_tuple, // Wrap and return a flag indicating overflow
checked, // Return std::nullopt on overflow/underflow
strict, // Call std::exit(EXIT_FAILURE) on error
widen, // Promote to the next wider type (add/mul only)
};
} // namespace boost::safe_numbers
Policy Summary
| Policy | Overflow/Underflow Behavior | Division by Zero | noexcept |
|---|---|---|---|
|
Throws exception |
Throws |
No |
|
Clamps to min/max |
Throws |
Add/Sub/Mul: Yes, Div/Mod: No |
|
Wraps, returns flag |
Throws |
Add/Sub/Mul: Yes, Div/Mod: No |
|
Returns |
Returns |
Yes |
|
Calls |
Calls |
Yes |
|
Promotes to next wider type (add/mul only) |
N/A (only add/mul supported) |
Yes |
Policies as Part of the Type
Every numeric basis template takes the policy as a second template parameter, defaulted to throw_exception:
namespace boost::safe_numbers::detail {
template <fundamental_unsigned_integral BasisType, auto ErrorPolicy = overflow_policy::throw_exception>
class unsigned_integer_basis;
template <fundamental_signed_integral BasisType, auto ErrorPolicy = overflow_policy::throw_exception>
class signed_integer_basis;
template <compatible_float_type BasisType, auto ErrorPolicy = overflow_policy::throw_exception>
class float_basis;
} // namespace boost::safe_numbers::detail
An operator on a policy-carrying type behaves exactly like the corresponding named free function: sat_u8 + sat_u8 is saturating_add, strict_i32 / strict_i32 is strict_div, and so on.
This includes the edge cases: division and modulo by zero still throw std::domain_error under saturate, and sat_i8{-128} / sat_i8{-1} clamps to 127.
Increment, decrement, compound assignment, and (for signed types) unary minus follow the policy as well.
Conversions and stream extraction always throw regardless of the policy.
Only policies whose result is the operand type can live in the type.
overflow_tuple, checked, and widen change the result type of every operation, so requesting them as a type-level policy is a compile error that points you at the overflowing_*, checked_*, and widening_* free functions.
strict is integer only.
The basic_* Alias Templates
Spelling the detail template with its underlying type is never necessary. Every width has an alias template that selects the policy by type, defaulting to throwing:
namespace boost::safe_numbers {
// Tag types selecting the built-in policies
struct throwing {};
struct saturating {};
struct strict {};
template <typename ErrorHandler = throwing>
using basic_u8 = detail::unsigned_integer_basis<std::uint8_t, detail::type_policy_v<ErrorHandler>>;
// ... basic_u16 through basic_u128, basic_i8 through basic_i128, basic_f32, basic_f64
} // namespace boost::safe_numbers
basic_u8<> is exactly u8, basic_u8<saturating> is exactly sat_u8, and basic_u8<strict> is exactly strict_u8; the same identities hold for every width.
Any other type argument is a user defined handler.
User Defined Handlers
A handler is a stateless class whose on_error decides the result of a failed operation:
#include <boost/safe_numbers/unsigned_integers.hpp>
// Wrapping semantics, like Rust's Wrapping<T>
struct wrapping_handler
{
template <typename T>
constexpr auto on_error(const boost::safe_numbers::error_kind,
const T value,
const char*) const noexcept -> T
{
return value;
}
};
using wrap_u32 = boost::safe_numbers::basic_u32<wrapping_handler>;
wrap_u32 a {4294967295U};
++a; // a == 0, no exception
The handler contract, checked by the error_handler_for concept:
-
The type must be empty (stateless): the policy is part of the type, so per-value state would be lost.
-
on_errormust be callable on aconstobject with(error_kind, BasisType, const char*)and returnBasisType. -
Whatever
on_errorreturns becomes the result of the operation. Throwing or terminating instead is equally valid.
The error_kind argument identifies the failure (overflow, underflow, divide_by_zero, and for floats nan_operation and invalid_operation), and the message is the same diagnostic string the throwing policy would have used.
The value argument is a defined fallback, chosen so that returning it unchanged gives wrapping semantics:
| Error site | Value handed to on_error |
|---|---|
Integer add, sub, mul, increment, decrement, unary minus, |
The two’s complement wrapped result |
Integer division or modulo by zero |
The dividend (left operand) |
Integer |
|
Unsigned shifts past the type width |
The shift with the amount reduced modulo the width |
Every float operator |
The raw IEEE 754 result (infinity, NaN, …) |
Behavioral notes:
-
noexceptof every operator follows the handler: anoexcepton_errormakes the arithmeticnoexcept. -
A handler that returns a value works in constant expressions; one that throws makes a constant-evaluated error a compile error, exactly like the throwing policy.
-
Handler-typed values do not mix with other policies or other handlers, and the compile-time overflow checks (Compile-Time Precondition Checks) do not fire for handler types since their results are defined by the handler.
-
On CUDA and SYCL devices the handler is called directly, so it must be usable in device code (a
constexprhandler generally is). -
Handlers work with every width, including the 128-bit types.
Provided Aliases
| Policy | Unsigned | Signed | Float |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
Not available |
The saturating float aliases perform raw IEEE 754 arithmetic: overflow saturates to infinity, NaN propagates, and division by zero yields infinity.
They run no checks, matching the value component of the overflowing_* functions bit for bit.
Mixed Policies
Types with different policies do not mix, exactly like types with different widths:
sat_u8 a {200};
u8 b {100};
auto c = a + b; // Compile error: convert explicitly through basis_type first
auto d = a + sat_u8{static_cast<std::uint8_t>(b)}; // OK
The named free functions accept any policy-carrying type and preserve its policy in the result, so overflowing_add(sat_u8{200}, sat_u8{100}) returns std::pair<sat_u8, bool>.
A generic call such as add<overflow_policy::throw_exception>(sat_a, sat_b) follows the requested call-site policy, not the type’s.
Behavioral Notes
-
noexceptfollows the policy:sat_u8addition isnoexcept, its division is not (division by zero still throws), and everystrictoperation isnoexceptbecause termination is not an exception. -
Saturating overflow is a defined value, so it works in constant expressions:
constexpr sat_u8 x {sat_u8{255} + sat_u8{1}};yields255. Underthrow_exceptionandstrictthe same expression remains a compile error. -
stricttypes are host only:std::exithas no meaning in CUDA or SYCL kernels, so using them in device code fails to compile. Thethrow_exceptionandsaturatetypes work on device, where throwing reports through the device error machinery as usual. -
The compile-time overflow checks (see Compile-Time Precondition Checks) fire for
throw_exceptionandstricttypes and are automatically excluded forsaturatetypes, whose results are always defined. -
The active policy is queryable as a static member:
sat_u8::error_policy == overflow_policy::saturate.
Named Arithmetic Functions
For cases where throwing exceptions is not desired, named free functions are provided for each policy.
Saturating Arithmetic
template <UnsignedLibType T>
constexpr T saturating_add(T lhs, T rhs) noexcept;
template <UnsignedLibType T>
constexpr T saturating_sub(T lhs, T rhs) noexcept;
template <UnsignedLibType T>
constexpr T saturating_mul(T lhs, T rhs) noexcept;
template <UnsignedLibType T>
constexpr T saturating_div(T lhs, T rhs);
template <UnsignedLibType T>
constexpr T saturating_mod(T lhs, T rhs);
These functions clamp the result to the representable range instead of throwing:
-
saturating_add: Returns the sum, saturating atstd::numeric_limits<T>::max()on overflow -
saturating_sub: Returns the difference, saturating atstd::numeric_limits<T>::min()(zero) on underflow -
saturating_mul: Returns the product, saturating atstd::numeric_limits<T>::max()on overflow -
saturating_div: Returns the quotient; throwsstd::domain_erroron division by zero (overflow is impossible) -
saturating_mod: Returns the remainder; throwsstd::domain_erroron division by zero (overflow is impossible)
Overflowing Arithmetic
template <UnsignedLibType T>
constexpr std::pair<T, bool> overflowing_add(T lhs, T rhs) noexcept;
template <UnsignedLibType T>
constexpr std::pair<T, bool> overflowing_sub(T lhs, T rhs) noexcept;
template <UnsignedLibType T>
constexpr std::pair<T, bool> overflowing_mul(T lhs, T rhs) noexcept;
template <UnsignedLibType T>
constexpr std::pair<T, bool> overflowing_div(T lhs, T rhs);
template <UnsignedLibType T>
constexpr std::pair<T, bool> overflowing_mod(T lhs, T rhs);
These functions provide well-defined wrapping semantics with a flag to indicate if overflow occurred.
This follows normal C family unsigned rollover where UINT_MAX + 1 == 0 and 0 - 1 == UINT_MAX.
-
overflowing_add: Returns the wrapped sum andtrueif overflow occurred -
overflowing_sub: Returns the wrapped difference andtrueif underflow occurred -
overflowing_mul: Returns the wrapped product andtrueif overflow occurred -
overflowing_div: Returns the quotient andfalse; throwsstd::domain_erroron division by zero -
overflowing_mod: Returns the remainder andfalse; throwsstd::domain_erroron division by zero
Checked Arithmetic
template <UnsignedLibType T>
constexpr std::optional<T> checked_add(T lhs, T rhs) noexcept;
template <UnsignedLibType T>
constexpr std::optional<T> checked_sub(T lhs, T rhs) noexcept;
template <UnsignedLibType T>
constexpr std::optional<T> checked_mul(T lhs, T rhs) noexcept;
template <UnsignedLibType T>
constexpr std::optional<T> checked_div(T lhs, T rhs) noexcept;
template <UnsignedLibType T>
constexpr std::optional<T> checked_mod(T lhs, T rhs) noexcept;
These functions return std::nullopt on overflow, underflow, or division by zero:
-
checked_add: Returns the sum, orstd::nullopton overflow -
checked_sub: Returns the difference, orstd::nullopton underflow -
checked_mul: Returns the product, orstd::nullopton overflow -
checked_div: Returns the quotient, orstd::nullopton division by zero -
checked_mod: Returns the remainder, orstd::nullopton division by zero
Strict Arithmetic
template <UnsignedLibType T>
constexpr T strict_add(T lhs, T rhs) noexcept;
template <UnsignedLibType T>
constexpr T strict_sub(T lhs, T rhs) noexcept;
template <UnsignedLibType T>
constexpr T strict_mul(T lhs, T rhs) noexcept;
template <UnsignedLibType T>
constexpr T strict_div(T lhs, T rhs) noexcept;
template <UnsignedLibType T>
constexpr T strict_mod(T lhs, T rhs) noexcept;
These functions call std::exit(EXIT_FAILURE) on error, providing a hard termination policy for safety-critical applications where exceptions cannot be used:
-
strict_add: Returns the sum; callsstd::exit(EXIT_FAILURE)on overflow -
strict_sub: Returns the difference; callsstd::exit(EXIT_FAILURE)on underflow -
strict_mul: Returns the product; callsstd::exit(EXIT_FAILURE)on overflow -
strict_div: Returns the quotient; callsstd::exit(EXIT_FAILURE)on division by zero -
strict_mod: Returns the remainder; callsstd::exit(EXIT_FAILURE)on modulo by zero
All strict functions are marked noexcept since std::exit does not throw.
Widening Arithmetic
template <UnsignedLibType T>
constexpr auto widening_add(T lhs, T rhs) noexcept;
template <UnsignedLibType T>
constexpr auto widening_mul(T lhs, T rhs) noexcept;
These functions avoid overflow entirely by promoting the result to the next wider unsigned integer type.
The promotion chain is: uint8 → u16, u16 → u32, u32 → u64, u64 → uint128.
Since uint128 is the widest supported type, widening is not available for uint128 operands (a static_assert fires).
Only addition and multiplication are provided because subtraction, division, and modulo cannot overflow into a range that requires a wider type.
-
widening_add: Returns the sum in the next wider type -
widening_mul: Returns the product in the next wider type
Both functions are noexcept.
Named Shift Functions
The same policy variants available for arithmetic operations are also available for shift operations.
The widen policy is not supported for shifts.
Overflow Conditions
-
Left shift (
<<): Overflow occurs whenbit_width(lhs) + rhs >= std::numeric_limits<BasisType>::digits(i.e., bits would be shifted past the type width). -
Right shift (
>>): Overflow occurs whenrhs >= std::numeric_limits<BasisType>::digits(i.e., the shift amount is greater than or equal to the type width).
Saturating Shifts
template <UnsignedLibType T>
constexpr T saturating_shl(T lhs, T rhs) noexcept;
template <UnsignedLibType T>
constexpr T saturating_shr(T lhs, T rhs) noexcept;
-
saturating_shl: Returns the shifted value, saturating atstd::numeric_limits<T>::max()on overflow -
saturating_shr: Returns the shifted value, saturating at0when the shift amount is >= the type width
Overflowing Shifts
template <UnsignedLibType T>
constexpr std::pair<T, bool> overflowing_shl(T lhs, T rhs) noexcept;
template <UnsignedLibType T>
constexpr std::pair<T, bool> overflowing_shr(T lhs, T rhs) noexcept;
-
overflowing_shl: Returns the wrapped shifted value andtrueif overflow occurred -
overflowing_shr: Returns0andtrueif the shift amount is >= the type width; otherwise the shifted value andfalse
Checked Shifts
template <UnsignedLibType T>
constexpr std::optional<T> checked_shl(T lhs, T rhs) noexcept;
template <UnsignedLibType T>
constexpr std::optional<T> checked_shr(T lhs, T rhs) noexcept;
-
checked_shl: Returns the shifted value, orstd::nullopton overflow -
checked_shr: Returns the shifted value, orstd::nulloptwhen the shift amount is >= the type width
Strict Shifts
template <UnsignedLibType T>
constexpr T strict_shl(T lhs, T rhs) noexcept;
template <UnsignedLibType T>
constexpr T strict_shr(T lhs, T rhs) noexcept;
-
strict_shl: Returns the shifted value; callsstd::exit(EXIT_FAILURE)on overflow -
strict_shr: Returns the shifted value; callsstd::exit(EXIT_FAILURE)when the shift amount is >= the type width
All shift policy functions are noexcept.
Generic Policy-Parameterized Arithmetic
template <overflow_policy Policy, UnsignedLibType T>
constexpr auto add(T lhs, T rhs);
template <overflow_policy Policy, UnsignedLibType T>
constexpr auto sub(T lhs, T rhs);
template <overflow_policy Policy, UnsignedLibType T>
constexpr auto mul(T lhs, T rhs);
template <overflow_policy Policy, UnsignedLibType T>
constexpr auto div(T lhs, T rhs);
template <overflow_policy Policy, UnsignedLibType T>
constexpr auto mod(T lhs, T rhs);
template <overflow_policy Policy, UnsignedLibType T>
constexpr auto shl(T lhs, T rhs);
template <overflow_policy Policy, UnsignedLibType T>
constexpr auto shr(T lhs, T rhs);
These functions accept an overflow_policy as a template parameter and dispatch to the corresponding named function.
The widen policy is not supported for shl or shr.
The return type depends on the policy:
| Policy | Return Type |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
Next wider unsigned integer type (add/mul only) |
This allows writing generic code parameterized on the overflow policy:
using namespace boost::safe_numbers;
// The policy can be a template parameter of your own function
template <overflow_policy Policy>
auto compute(u32 a, u32 b)
{
return add<Policy>(a, b);
}
auto result_sat = compute<overflow_policy::saturate>(u32{100}, u32{200});
auto result_chk = compute<overflow_policy::checked>(u32{100}, u32{200});
Exception Summary
The default operators and some named functions throw exceptions on error:
| Operation | Exception Type | Condition |
|---|---|---|
|
|
Result exceeds maximum value |
|
|
Result would be negative |
|
|
Result exceeds maximum value |
|
|
Division by zero |
|
|
Modulo by zero |
|
|
Value is at maximum |
|
|
Value is zero |
|
|
|
|
|
|
|
|
Division by zero |
|
|
Division by zero |