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_u8 and strict_i32. The default arithmetic operators (+, -, *, /, %) and shift operators (<<, >>) follow the type’s policy, which is throw_exception for 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

throw_exception (default)

Throws exception

Throws std::domain_error

No

saturate

Clamps to min/max

Throws std::domain_error

Add/Sub/Mul: Yes, Div/Mod: No

overflow_tuple

Wraps, returns flag

Throws std::domain_error

Add/Sub/Mul: Yes, Div/Mod: No

checked

Returns std::nullopt

Returns std::nullopt

Yes

strict

Calls std::exit(EXIT_FAILURE)

Calls std::exit(EXIT_FAILURE)

Yes

widen

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_error must be callable on a const object with (error_kind, BasisType, const char*) and return BasisType.

  • Whatever on_error returns 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, MIN / -1

The two’s complement wrapped result

Integer division or modulo by zero

The dividend (left operand)

Integer MIN % -1

0 (the mathematical result)

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:

  • noexcept of every operator follows the handler: a noexcept on_error makes the arithmetic noexcept.

  • 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 constexpr handler generally is).

  • Handlers work with every width, including the 128-bit types.

Provided Aliases

Policy Unsigned Signed Float

throw_exception (default)

u8, u16, u32, u64, u128

i8, i16, i32, i64, i128

f32, f64

saturate

sat_u8, sat_u16, sat_u32, sat_u64, sat_u128

sat_i8, sat_i16, sat_i32, sat_i64, sat_i128

sat_f32, sat_f64

strict

strict_u8, strict_u16, strict_u32, strict_u64, strict_u128

strict_i8, strict_i16, strict_i32, strict_i64, strict_i128

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

  • noexcept follows the policy: sat_u8 addition is noexcept, its division is not (division by zero still throws), and every strict operation is noexcept because 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}}; yields 255. Under throw_exception and strict the same expression remains a compile error.

  • strict types are host only: std::exit has no meaning in CUDA or SYCL kernels, so using them in device code fails to compile. The throw_exception and saturate types 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_exception and strict types and are automatically excluded for saturate types, 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 at std::numeric_limits<T>::max() on overflow

  • saturating_sub: Returns the difference, saturating at std::numeric_limits<T>::min() (zero) on underflow

  • saturating_mul: Returns the product, saturating at std::numeric_limits<T>::max() on overflow

  • saturating_div: Returns the quotient; throws std::domain_error on division by zero (overflow is impossible)

  • saturating_mod: Returns the remainder; throws std::domain_error on 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 and true if overflow occurred

  • overflowing_sub: Returns the wrapped difference and true if underflow occurred

  • overflowing_mul: Returns the wrapped product and true if overflow occurred

  • overflowing_div: Returns the quotient and false; throws std::domain_error on division by zero

  • overflowing_mod: Returns the remainder and false; throws std::domain_error on 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, or std::nullopt on overflow

  • checked_sub: Returns the difference, or std::nullopt on underflow

  • checked_mul: Returns the product, or std::nullopt on overflow

  • checked_div: Returns the quotient, or std::nullopt on division by zero

  • checked_mod: Returns the remainder, or std::nullopt on 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; calls std::exit(EXIT_FAILURE) on overflow

  • strict_sub: Returns the difference; calls std::exit(EXIT_FAILURE) on underflow

  • strict_mul: Returns the product; calls std::exit(EXIT_FAILURE) on overflow

  • strict_div: Returns the quotient; calls std::exit(EXIT_FAILURE) on division by zero

  • strict_mod: Returns the remainder; calls std::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: uint8u16, u16u32, u32u64, u64uint128. 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 when bit_width(lhs) + rhs >= std::numeric_limits<BasisType>::digits (i.e., bits would be shifted past the type width).

  • Right shift (>>): Overflow occurs when rhs >= 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 at std::numeric_limits<T>::max() on overflow

  • saturating_shr: Returns the shifted value, saturating at 0 when 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 and true if overflow occurred

  • overflowing_shr: Returns 0 and true if the shift amount is >= the type width; otherwise the shifted value and false

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, or std::nullopt on overflow

  • checked_shr: Returns the shifted value, or std::nullopt when 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; calls std::exit(EXIT_FAILURE) on overflow

  • strict_shr: Returns the shifted value; calls std::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

overflow_policy::throw_exception

T

overflow_policy::saturate

T

overflow_policy::overflow_tuple

std::pair<T, bool>

overflow_policy::checked

std::optional<T>

overflow_policy::strict

T

overflow_policy::widen

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

+, +=

std::overflow_error

Result exceeds maximum value

-, -=

std::underflow_error

Result would be negative

*, *=

std::overflow_error

Result exceeds maximum value

/, /=

std::domain_error

Division by zero

%, %=

std::domain_error

Modulo by zero

++ (pre/post)

std::overflow_error

Value is at maximum

-- (pre/post)

std::underflow_error

Value is zero

<<, <⇐

std::overflow_error

bit_width(lhs) + rhs >= digits

>>, >>=

std::overflow_error

rhs >= digits

saturating_div, saturating_mod

std::domain_error

Division by zero

overflowing_div, overflowing_mod

std::domain_error

Division by zero