Unsigned Integer Types

Description

The library provides safe unsigned integer types that detect overflow, underflow, and other undefined behavior at runtime. These types are drop-in replacements for the standard unsigned integer types with added safety guarantees.

Type Underlying Type Width Min Max

u8

std::uint8_t

8 bits

0

255

u16

std::uint16_t

16 bits

0

65,535

u32

std::uint32_t

32 bits

0

4,294,967,295

u64

std::uint64_t

64 bits

0

18,446,744,073,709,551,615

u128

uint128_t

128 bits

0

340,282,366,920,938,463,463,374,607,431,768,211,455

Each type exposes a basis_type member type alias that refers to the underlying integer type, allowing conversion back to built-in types when needed.

#include <boost/safe_numbers/unsigned_integers.hpp>

namespace boost::safe_numbers {

using u8   = detail::unsigned_integer_basis<std::uint8_t>;
using u16  = detail::unsigned_integer_basis<std::uint16_t>;
using u32  = detail::unsigned_integer_basis<std::uint32_t>;
using u64  = detail::unsigned_integer_basis<std::uint64_t>;
using u128 = detail::unsigned_integer_basis<int128::uint128_t>;

template <unsigned_integral BasisType>
class unsigned_integer_basis {

public:
    using basis_type = BasisType;

    // Construction
    constexpr unsigned_integer_basis() noexcept = default;
    explicit constexpr unsigned_integer_basis(BasisType val) noexcept;

    template <typename T>
        requires std::is_same_v<T, bool>
    explicit constexpr unsigned_integer_basis(T) noexcept = delete; // bool prohibited

    // Conversion to underlying types
    template <unsigned_integral OtherBasis>
    explicit constexpr operator OtherBasis() const;

    // Comparison operators
    friend constexpr auto operator<=>(unsigned_integer_basis lhs, unsigned_integer_basis rhs) noexcept
        -> std::strong_ordering = default;

    // Compound assignment operators (arithmetic)
    template <unsigned_integral OtherBasis>
    constexpr auto operator+=(unsigned_integer_basis<OtherBasis> rhs) -> unsigned_integer_basis&;

    template <unsigned_integral OtherBasis>
    constexpr auto operator-=(unsigned_integer_basis<OtherBasis> rhs) -> unsigned_integer_basis&;

    template <unsigned_integral OtherBasis>
    constexpr auto operator*=(unsigned_integer_basis<OtherBasis> rhs) -> unsigned_integer_basis&;

    template <unsigned_integral OtherBasis>
    constexpr auto operator/=(unsigned_integer_basis<OtherBasis> rhs) -> unsigned_integer_basis&;

    template <unsigned_integral OtherBasis>
    constexpr auto operator%=(unsigned_integer_basis<OtherBasis> rhs) -> unsigned_integer_basis&;

    // Compound assignment operators (bitwise)
    constexpr auto operator&=(unsigned_integer_basis rhs) noexcept -> unsigned_integer_basis&;
    constexpr auto operator|=(unsigned_integer_basis rhs) noexcept -> unsigned_integer_basis&;
    constexpr auto operator^=(unsigned_integer_basis rhs) noexcept -> unsigned_integer_basis&;
    constexpr auto operator<<=(unsigned_integer_basis rhs) -> unsigned_integer_basis&;
    constexpr auto operator>>=(unsigned_integer_basis rhs) -> unsigned_integer_basis&;

    // Increment and decrement operators
    constexpr auto operator++() -> unsigned_integer_basis&;
    constexpr auto operator++(int) -> unsigned_integer_basis;
    constexpr auto operator--() -> unsigned_integer_basis&;
    constexpr auto operator--(int) -> unsigned_integer_basis;

    // Unary operators
    constexpr auto operator+() const noexcept -> unsigned_integer_basis;
    constexpr auto operator-() const noexcept; // compile-time error

}; // class unsigned_integer_basis

// Arithmetic operators (throw on overflow/underflow)
template <unsigned_integral BasisType>
constexpr auto operator+(unsigned_integer_basis<BasisType> lhs,
                         unsigned_integer_basis<BasisType> rhs) -> unsigned_integer_basis<BasisType>;

template <unsigned_integral BasisType>
constexpr auto operator-(unsigned_integer_basis<BasisType> lhs,
                         unsigned_integer_basis<BasisType> rhs) -> unsigned_integer_basis<BasisType>;

template <unsigned_integral BasisType>
constexpr auto operator*(unsigned_integer_basis<BasisType> lhs,
                         unsigned_integer_basis<BasisType> rhs) -> unsigned_integer_basis<BasisType>;

template <unsigned_integral BasisType>
constexpr auto operator/(unsigned_integer_basis<BasisType> lhs,
                         unsigned_integer_basis<BasisType> rhs) -> unsigned_integer_basis<BasisType>;

template <unsigned_integral BasisType>
constexpr auto operator%(unsigned_integer_basis<BasisType> lhs,
                         unsigned_integer_basis<BasisType> rhs) -> unsigned_integer_basis<BasisType>;

// Bitwise operators
template <unsigned_integral BasisType>
constexpr auto operator~(unsigned_integer_basis<BasisType> lhs) noexcept
    -> unsigned_integer_basis<BasisType>;

template <unsigned_integral BasisType>
constexpr auto operator&(unsigned_integer_basis<BasisType> lhs,
                         unsigned_integer_basis<BasisType> rhs) noexcept
    -> unsigned_integer_basis<BasisType>;

template <unsigned_integral BasisType>
constexpr auto operator|(unsigned_integer_basis<BasisType> lhs,
                         unsigned_integer_basis<BasisType> rhs) noexcept
    -> unsigned_integer_basis<BasisType>;

template <unsigned_integral BasisType>
constexpr auto operator^(unsigned_integer_basis<BasisType> lhs,
                         unsigned_integer_basis<BasisType> rhs) noexcept
    -> unsigned_integer_basis<BasisType>;

template <unsigned_integral BasisType>
constexpr auto operator<<(unsigned_integer_basis<BasisType> lhs,
                          unsigned_integer_basis<BasisType> rhs)
    -> unsigned_integer_basis<BasisType>;

template <unsigned_integral BasisType>
constexpr auto operator>>(unsigned_integer_basis<BasisType> lhs,
                          unsigned_integer_basis<BasisType> rhs)
    -> unsigned_integer_basis<BasisType>;

// Saturating arithmetic (clamp to min/max on overflow/underflow)
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);

// Overflowing arithmetic (wrap and return overflow flag)
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);

// Checked arithmetic (return std::nullopt on overflow/underflow)
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;

// Wrapping arithmetic (wrap without indication)
template <UnsignedLibType T>
constexpr T wrapping_add(T lhs, T rhs) noexcept;

template <UnsignedLibType T>
constexpr T wrapping_sub(T lhs, T rhs) noexcept;

template <UnsignedLibType T>
constexpr T wrapping_mul(T lhs, T rhs) noexcept;

template <UnsignedLibType T>
constexpr T wrapping_div(T lhs, T rhs);

template <UnsignedLibType T>
constexpr T wrapping_mod(T lhs, T rhs);

// Strict arithmetic (call std::exit(EXIT_FAILURE) on error)
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;

// Saturating shifts (clamp to max/0 on overflow)
template <UnsignedLibType T>
constexpr T saturating_shl(T lhs, T rhs) noexcept;

template <UnsignedLibType T>
constexpr T saturating_shr(T lhs, T rhs) noexcept;

// Overflowing shifts (wrap and return overflow flag)
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;

// Checked shifts (return std::nullopt on overflow)
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;

// Wrapping shifts (wrap without indication)
template <UnsignedLibType T>
constexpr T wrapping_shl(T lhs, T rhs) noexcept;

template <UnsignedLibType T>
constexpr T wrapping_shr(T lhs, T rhs) noexcept;

// Strict shifts (call std::exit(EXIT_FAILURE) on error)
template <UnsignedLibType T>
constexpr T strict_shl(T lhs, T rhs) noexcept;

template <UnsignedLibType T>
constexpr T strict_shr(T lhs, T rhs) 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);

// Generic policy-parameterized shifts
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);

} // namespace boost::safe_numbers

Operator Behavior

Default Construction

constexpr unsigned_integer_basis() noexcept = default;

Values are default-initialized to zero.

Construction from Underlying Type

explicit constexpr unsigned_integer_basis(BasisType val) noexcept;

Construction from the underlying type is explicit to prevent accidental conversions.

Construction from bool

template <typename T>
    requires std::is_same_v<T, bool>
explicit constexpr unsigned_integer_basis(T) noexcept = delete;

Constructing from bool is a compile-time error.

Conversion to Underlying Types

template <unsigned_integral OtherBasis>
explicit constexpr operator OtherBasis() const;

Conversion to other unsigned integral types is explicit. Widening conversions (e.g., u8 to u16) always succeed. Narrowing conversions (e.g., u32 to u16) perform a runtime bounds check: if the value exceeds the maximum representable value of the target type, std::domain_error is thrown. This allows safe narrowing when the value is known to fit at runtime.

Comparison Operators

friend constexpr auto operator<=>(unsigned_integer_basis lhs, unsigned_integer_basis rhs) noexcept
    -> std::strong_ordering = default;

Full three-way comparison is supported via operator<=>, which returns std::strong_ordering. All comparison operators (<, , >, >=, ==, !=) are available.

Arithmetic Operators

template <unsigned_integral BasisType>
constexpr auto operator+(unsigned_integer_basis<BasisType> lhs,
                         unsigned_integer_basis<BasisType> rhs) -> unsigned_integer_basis<BasisType>;

template <unsigned_integral BasisType>
constexpr auto operator-(unsigned_integer_basis<BasisType> lhs,
                         unsigned_integer_basis<BasisType> rhs) -> unsigned_integer_basis<BasisType>;

template <unsigned_integral BasisType>
constexpr auto operator*(unsigned_integer_basis<BasisType> lhs,
                         unsigned_integer_basis<BasisType> rhs) -> unsigned_integer_basis<BasisType>;

template <unsigned_integral BasisType>
constexpr auto operator/(unsigned_integer_basis<BasisType> lhs,
                         unsigned_integer_basis<BasisType> rhs) -> unsigned_integer_basis<BasisType>;

template <unsigned_integral BasisType>
constexpr auto operator%(unsigned_integer_basis<BasisType> lhs,
                         unsigned_integer_basis<BasisType> rhs) -> unsigned_integer_basis<BasisType>;

All arithmetic operators perform runtime checks and throw exceptions when undefined behavior would occur:

  • +: Throws std::overflow_error if the result exceeds the maximum representable value

  • -: Throws std::underflow_error if the result would be negative (wrap around)

  • *: Throws std::overflow_error if the result exceeds the maximum representable value

  • /: Throws std::domain_error if dividing by zero

  • %: Throws std::domain_error if the divisor is zero

Compound Assignment Operators

template <unsigned_integral OtherBasis>
constexpr auto operator+=(unsigned_integer_basis<OtherBasis> rhs) -> unsigned_integer_basis&;

template <unsigned_integral OtherBasis>
constexpr auto operator-=(unsigned_integer_basis<OtherBasis> rhs) -> unsigned_integer_basis&;

template <unsigned_integral OtherBasis>
constexpr auto operator*=(unsigned_integer_basis<OtherBasis> rhs) -> unsigned_integer_basis&;

template <unsigned_integral OtherBasis>
constexpr auto operator/=(unsigned_integer_basis<OtherBasis> rhs) -> unsigned_integer_basis&;

template <unsigned_integral OtherBasis>
constexpr auto operator%=(unsigned_integer_basis<OtherBasis> rhs) -> unsigned_integer_basis&;

Compound assignment operators follow the same exception behavior as their corresponding arithmetic operators.

Bitwise Operators

template <unsigned_integral BasisType>
constexpr auto operator~(unsigned_integer_basis<BasisType> lhs) noexcept
    -> unsigned_integer_basis<BasisType>;

template <unsigned_integral BasisType>
constexpr auto operator&(unsigned_integer_basis<BasisType> lhs,
                         unsigned_integer_basis<BasisType> rhs) noexcept
    -> unsigned_integer_basis<BasisType>;

template <unsigned_integral BasisType>
constexpr auto operator|(unsigned_integer_basis<BasisType> lhs,
                         unsigned_integer_basis<BasisType> rhs) noexcept
    -> unsigned_integer_basis<BasisType>;

template <unsigned_integral BasisType>
constexpr auto operator^(unsigned_integer_basis<BasisType> lhs,
                         unsigned_integer_basis<BasisType> rhs) noexcept
    -> unsigned_integer_basis<BasisType>;

template <unsigned_integral BasisType>
constexpr auto operator<<(unsigned_integer_basis<BasisType> lhs,
                          unsigned_integer_basis<BasisType> rhs)
    -> unsigned_integer_basis<BasisType>;

template <unsigned_integral BasisType>
constexpr auto operator>>(unsigned_integer_basis<BasisType> lhs,
                          unsigned_integer_basis<BasisType> rhs)
    -> unsigned_integer_basis<BasisType>;

The bitwise NOT, AND, OR, and XOR operators (~, &, |, ^) are noexcept and operate directly on the underlying values, since these operations cannot overflow.

The shift operators (<<, >>) perform runtime bounds checking:

  • <<: Throws std::overflow_error if the left shift would move bits past the type width. Specifically, this occurs when bit_width(lhs) + rhs >= std::numeric_limits<BasisType>::digits.

  • >>: Throws std::overflow_error if the shift amount is greater than or equal to the type width (i.e., rhs >= std::numeric_limits<BasisType>::digits).

Shift Overflow Policies

In addition to the default throwing behavior, the shift operators support the same overflow policies as arithmetic operations. The overflow condition for left shift is when bit_width(lhs) + rhs >= digits, and for right shift is when rhs >= digits.

// Saturating: clamp to max (left shift) or 0 (right shift) on overflow
constexpr T saturating_shl(T lhs, T rhs) noexcept;
constexpr T saturating_shr(T lhs, T rhs) noexcept;

// Overflowing: wrap and return an overflow flag
constexpr std::pair<T, bool> overflowing_shl(T lhs, T rhs) noexcept;
constexpr std::pair<T, bool> overflowing_shr(T lhs, T rhs) noexcept;

// Checked: return std::nullopt on overflow
constexpr std::optional<T> checked_shl(T lhs, T rhs) noexcept;
constexpr std::optional<T> checked_shr(T lhs, T rhs) noexcept;

// Wrapping: perform the shift ignoring overflow
constexpr T wrapping_shl(T lhs, T rhs) noexcept;
constexpr T wrapping_shr(T lhs, T rhs) noexcept;

// Strict: call std::exit(EXIT_FAILURE) on overflow
constexpr T strict_shl(T lhs, T rhs) noexcept;
constexpr T strict_shr(T lhs, T rhs) noexcept;

// Generic policy-parameterized shifts
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);

The behavior of each policy on shift overflow:

Policy Left Shift Overflow Right Shift Overflow

saturating

Returns std::numeric_limits<T>::max()

Returns 0

overflowing

Returns the wrapped result with true

Returns 0 with true

checked

Returns std::nullopt

Returns std::nullopt

wrapping

Performs the shift (bits shifted out are lost)

Returns 0

strict

Calls std::exit(EXIT_FAILURE)

Calls std::exit(EXIT_FAILURE)

All shift policy functions are noexcept.

Compound Bitwise Assignment Operators

constexpr auto operator&=(unsigned_integer_basis rhs) noexcept -> unsigned_integer_basis&;
constexpr auto operator|=(unsigned_integer_basis rhs) noexcept -> unsigned_integer_basis&;
constexpr auto operator^=(unsigned_integer_basis rhs) noexcept -> unsigned_integer_basis&;
constexpr auto operator<<=(unsigned_integer_basis rhs) -> unsigned_integer_basis&;
constexpr auto operator>>=(unsigned_integer_basis rhs) -> unsigned_integer_basis&;

Compound bitwise assignment operators delegate to the corresponding free-function bitwise operators and follow the same exception behavior. &=, |=, and ^= are noexcept. <⇐ and >>= throw std::overflow_error under the same conditions as << and >>.

Increment and Decrement Operators

constexpr auto operator++() -> unsigned_integer_basis&;
constexpr auto operator++(int) -> unsigned_integer_basis;
constexpr auto operator--() -> unsigned_integer_basis&;
constexpr auto operator--(int) -> unsigned_integer_basis;
  • ++ (pre/post): Throws std::overflow_error if the value is already at the maximum

  • -- (pre/post): Throws std::underflow_error if the value is already zero

Unary Operators

constexpr auto operator+() const noexcept -> unsigned_integer_basis;
constexpr auto operator-() const noexcept; // compile-time error
  • +: Returns a copy of the value (identity). This is consistent with built-in unsigned integer behavior.

  • -: Deliberately disabled. Any use of unary minus on an unsigned safe integer is a compile-time error via static_assert. While C++ defines unary minus on unsigned integers as modular negation (2^N - x), this is a common source of bugs and is prohibited by this library. Use a wrapping subtraction policy from zero or the maximum value if modular negation is needed.

Mixed-Width Operations

Operations between different width safe integer types are compile-time errors. To perform operations between different widths, explicitly convert to the same type first.

Alternative Arithmetic and Shift Functions

For cases where throwing exceptions is not desired, alternative arithmetic and shift functions are provided with different overflow handling policies. See Overflow Policies for full documentation of all policies, named functions, and the generic policy-parameterized interface.

Constexpr Support

All operations are constexpr-compatible. Overflow at compile time results in a compiler error.