Comparisons

Boost.SafeNumbers is one of several efforts to make numeric code safer. This page first situates the library among the existing C++ safe-integer libraries and the strong-typing facilities of other languages, then compares Boost.SafeNumbers and Boost.SafeNumerics in detail.

Feature Matrix

The matrix below compares Boost.SafeNumbers against the closest C++ libraries (Boost.SafeNumerics, bounded::integer, and type_safe) and against the strong integer typing found in Ada/SPARK and Rust. "Yes" and "No" describe the default, out-of-the-box behavior; cells note important qualifications inline.

Feature Boost.SafeNumbers Boost.SafeNumerics bounded::integer type_safe Ada / SPARK Rust

Language / minimum standard

C++20

C++14

Modern C++

C++11

Ada (SPARK subset)

Rust

Default behavior on overflow

Throws std::overflow_error / std::underflow_error at the declared width.

Policy-dependent; the default native promotion can absorb overflow into a wider type, while the exception policy throws at runtime.

Cannot overflow by construction; the result type’s bounds are widened at compile time.

Undefined behavior (the arithmetic policy is configurable).

Constraint_Error at runtime; GNATprove proves absence statically.

Panics in debug, wraps in release; checked_ / saturating_ / wrapping_ methods are explicit.

Alternative overflow handling

Per-operation functions: saturating_*, checked_*, overflowing_*, plus the throwing default.

Selected as a template parameter on the type (safe<T, PP, EP>); no per-operation functions.

Prevented by construction; explicit clamp / wrap helpers.

Arithmetic policy (undefined or checked) selectable.

Range subtypes, mod types, and library saturation.

checked_ / saturating_ / wrapping_ / overflowing_ methods on the primitive types.

Range-constrained (bounded) types

Yes: bounded_uint / bounded_int / bounded_float, with bounds enforced on every arithmetic result.

Yes: safe_unsigned_range / safe_signed_range, but bounds are enforced on construction and assignment only (arithmetic results may exceed them).

Yes: bounds are the core abstraction, tracked and widened at compile time.

constrained_type with a runtime-checked bound.

Yes: first-class subtype range constraints, statically proven by GNATprove.

No ranged integer type; only NonZero; ranges via external crates.

Implicit widening / mixed-width arithmetic

Compile-time error; both operands must be the same type.

Allowed; operands are promoted following C++ rules.

Allowed; result bounds are computed from the operand bounds.

Lossy and mixed-signedness operations are rejected.

Cross-subtype operations require an explicit conversion.

No implicit numeric conversion; explicit as / From.

Implicit conversion to/from built-in types

Compile-time error; explicit only.

Allowed, including from bool.

Constructs only from values within range.

Lossy and implicit conversions rejected; explicit only.

Explicit conversions required.

Explicit only.

Compile-time-only verified construction (consteval)

Yes: consteval constructors and arithmetic, constexpr conversions and comparisons.

constexpr-capable; the default policy dispatches checks at runtime.

constexpr-friendly; bound tracking is a compile-time computation.

constexpr-capable.

Static checking at compile and proof time (a different model).

const fn and const generics where values are const.

Formal verification of the arithmetic

Yes (model level): the fundamental operations (add, sub, mul, div, mod for signed and unsigned) were re-implemented in the Why3 platform and proven correct, and the C is a faithful transcription of the verified algorithms. The shipped C source is not itself mechanically verified.

No formal-verification toolchain.

No.

No.

Yes (source level): GNATprove (built on Why3) proves absence of overflow, range violation, and division by zero in the Ada source itself.

No deductive proof in the standard library; type-system guarantees only (external tools such as Kani and Creusot are separate).

GPU / CUDA device support

Yes: usable from CUDA device code, with CUDA error reporting.

No.

No.

No.

No.

Via external crates only; not in the standard library.

Floating-point safe types

Yes: safe floating-point types and bounded_float.

Integer-focused; no safe floating-point type.

Integers only.

Yes: floating_point<T> wrapper.

Yes: floating-point and fixed-point subtypes with range constraints.

Primitives only; no ranged float type.

Compile-time integer range and overflow safety is well-established prior art: Boost.SafeNumerics, bounded::integer, and type_safe all track bounds or reject unsafe conversions, and Ada/SPARK and Rust provide strong integer typing at the language level. What distinguishes Boost.SafeNumbers in the table above is the combination of consteval-enforced types, fundamental operations whose algorithms are proven correct in Why3, and usability from CUDA device code in a single header-only C++20 library.

The remainder of this page compares Boost.SafeNumbers and Boost.SafeNumerics in detail.

Key Differences

Feature Boost.SafeNumbers Boost.SafeNumerics

Default overflow behavior

Throws std::overflow_error / std::underflow_error immediately at the declared width.

With the default native promotion policy, operands are promoted following C++ integer promotion rules. For small types like uint8_t, this means overflow is absorbed silently (e.g. safe<uint8_t>(255) + safe<uint8_t>(1) produces 256 as an int).

Construction

Explicit only. Construction from bool is a compile-time error.

Implicit construction from built-in types is allowed, including from bool.

Overflow policies

Named free functions per-operation: saturating_add, saturating_sub, checked_mul, overflowing_div, etc. The default operator always throws.

Policy is selected as a template parameter on the type itself (safe<T, PromotionPolicy, ExceptionPolicy>). No per-operation policy functions.

Mixed-width arithmetic

Compile-time error. Both operands must be the same type.

Allowed. Operands are promoted following C++ rules.

Mixed safe/built-in arithmetic

Compile-time error. Built-in types cannot be used as operands with safe types.

Allowed. The built-in operand is implicitly accepted.

Unary minus on unsigned

Compile-time error via static_assert.

Allowed with native promotion: promotes to signed int and negates (e.g. -safe<uint8_t>(5) produces -5).

Narrowing conversions

Explicit static_cast required. Throws std::domain_error if the value does not fit.

Both implicit and explicit narrowing throw std::system_error if the value does not fit.

Bounded / range-constrained types

bounded_uint<Min, Max> enforces bounds on every arithmetic result. 60 + 50 on a [0, 100] range throws immediately.

safe_unsigned_range<Min, Max> only enforces bounds on construction and assignment. Arithmetic results are promoted and can silently exceed the declared range (e.g. 60 + 50 = 110 on a [0, 100] range succeeds).

C++ standard required

C++20

C++14

Type Deduction Readability

Another practical difference is how each library’s types appear in IDE tooltips and debugger output. The deduced type for bounded arithmetic results illustrates this clearly.

Boost.SafeNumbers deduces a clean, readable type:

SafeNumbers bounded_uint type deduction

Boost.SafeNumerics deduces a deeply nested template type that can be difficult to read at a glance:

SafeNumerics safe_unsigned_range type deduction

Runnable Example

Example 1. This example demonstrates each of the differences listed above side-by-side.
// Copyright 2026 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

// This example compares the behavior of Boost.SafeNumbers unsigned integer
// types against Boost.SafeNumerics for common unsigned arithmetic scenarios.
//
// Key differences:
//   1. SafeNumbers always operates at the declared width - u8 + u8 stays u8.
//      SafeNumerics with the default "native" promotion policy follows C++
//      integer promotion rules, so safe<uint8_t> + safe<uint8_t> silently
//      promotes to int. This means many overflow cases go undetected.
//   2. SafeNumbers requires explicit construction and forbids implicit
//      conversions; SafeNumerics allows implicit construction from built-ins.
//   3. SafeNumbers provides named free functions for alternative overflow
//      policies (saturating_add, saturating_sub, checked_mul, etc.);
//      SafeNumerics selects behavior via template policy parameters on the type.
//   4. SafeNumbers forbids mixed-width arithmetic at compile time;
//      SafeNumerics promotes operands using C++ native promotion rules.
//   5. SafeNumbers forbids unary minus on unsigned types at compile time;
//      SafeNumerics promotes and allows it silently.

#include <boost/safe_numbers/unsigned_integers.hpp>
#include <boost/safe_numbers/bounded_integers.hpp>
#include <boost/safe_numbers/iostream.hpp>

// Warning suppression required for safe_numerics
#if defined (__clang__)
#  pragma clang diagnostic ignored "-Wold-style-cast"
#elif defined(__GNUC__)
#  pragma GCC diagnostic push
#  pragma GCC diagnostic ignored "-Wold-style-cast"
#  pragma GCC diagnostic ignored "-Wundef"
#  if __GNUC__ >= 16
#    pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
#  endif
#elif defined(_MSC_VER)
#  pragma warning(push)
#  pragma warning(disable : 4804) // Unsafe use of type bool in operation
#  pragma warning(disable : 4702) // Unreachable code
#endif

// Workaround for GCC-11 and GCC-12 not following the -Wundef pragma above
#ifndef BOOST_CLANG
#  define BOOST_CLANG 0
#endif

#include <boost/safe_numerics/safe_integer.hpp>
#include <boost/safe_numerics/safe_integer_range.hpp>

#include <iostream>
#include <cstdint>

int main()
{
    namespace safe_num = boost::safe_numbers;
    namespace safe_nrc = boost::safe_numerics;

    // -----------------------------------------------------------------------
    // 1. Construction
    // -----------------------------------------------------------------------
    std::cout << "--- Construction ---\n";

    // SafeNumbers: explicit construction only
    {
        const auto x = safe_num::u8{200U};
        std::cout << "safe_numbers explicit: u8{200} = " << x << std::endl;
    }

    // SafeNumerics: implicit construction from built-in types is allowed
    {
        safe_nrc::safe<std::uint8_t> x = 200;
        std::cout << "safe_numerics implicit: u8 = 200 -> "
                  << static_cast<int>(x) << std::endl;
    }

    // SafeNumbers forbids construction from bool (compile error):
    //   safe_num::u8 from_bool{true};  // static_assert failure

    // SafeNumerics allows it:
    {
        safe_nrc::safe<std::uint8_t> from_bool = true;  // OK, value is 1
        std::cout << "safe_numerics from bool: " << static_cast<int>(from_bool) << std::endl;
    }

    // -----------------------------------------------------------------------
    // 2. Overflow detection - the critical difference
    // -----------------------------------------------------------------------
    // SafeNumbers always operates at the declared width.
    // SafeNumerics with "native" policy follows C++ integer promotion:
    //   safe<uint8_t> + safe<uint8_t> promotes to int, so 255 + 1 = 256
    //   which fits in int - NO exception is thrown!
    std::cout << "\n--- Overflow on addition ---\n";

    // SafeNumbers: u8 + u8 stays u8, 255 + 1 overflows -> throws
    try
    {
        const auto result = safe_num::u8{255U} + safe_num::u8{1U};
        std::cout << "safe_numbers: " << result << std::endl;
    }
    catch (const std::overflow_error& e)
    {
        std::cout << "safe_numbers threw: " << e.what() << std::endl;
    }

    // SafeNumerics: uint8_t promotes to int, so 255 + 1 = 256, no overflow
    try
    {
        safe_nrc::safe<std::uint8_t> x = 255;
        safe_nrc::safe<std::uint8_t> y = 1;
        auto result = x + y;  // result is safe<int> with value 256
        std::cout << "safe_numerics: " << static_cast<int>(result) << std::endl;
        // Output: 256 (promoted to int, no overflow detected!)
    }
    catch (const std::exception& e)
    {
        std::cout << "safe_numerics threw: " << e.what() << std::endl;
    }

    // -----------------------------------------------------------------------
    // 3. Underflow detection on subtraction
    // -----------------------------------------------------------------------
    // Same promotion issue: 0u8 - 1u8 promotes to int, giving -1
    std::cout << "\n--- Underflow on subtraction ---\n";

    // SafeNumbers: u8 - u8 stays u8, 0 - 1 underflows -> throws
    try
    {
        const auto result = safe_num::u8{0U} - safe_num::u8{1U};
        std::cout << "safe_numbers: " << result << std::endl;
    }
    catch (const std::underflow_error& e)
    {
        std::cout << "safe_numbers threw: " << e.what() << std::endl;
    }

    // SafeNumerics: uint8_t promotes to int, so 0 - 1 = -1, no underflow
    try
    {
        safe_nrc::safe<std::uint8_t> x = 0;
        safe_nrc::safe<std::uint8_t> y = 1;
        auto result = x - y;  // result is safe<int> with value -1
        std::cout << "safe_numerics: " << static_cast<int>(result) << std::endl;
        // Output: -1 (promoted to int, no underflow detected!)
    }
    catch (const std::exception& e)
    {
        std::cout << "safe_numerics threw: " << e.what() << std::endl;
    }

    // -----------------------------------------------------------------------
    // 4. Alternative overflow policies (SafeNumbers only)
    // -----------------------------------------------------------------------
    // SafeNumbers provides named free functions for each policy.
    // SafeNumerics has no equivalent - behavior is fixed at the type level.
    std::cout << "\n--- Alternative policies (SafeNumbers) ---\n";

    {
        const auto x = safe_num::u8{250U};
        const auto y = safe_num::u8{10U};

        // Saturating: clamp at the max/min boundary
        const auto saturated = safe_num::saturating_add(x, y);
        std::cout << "saturating_add(250, 10) = " << saturated << std::endl;
        // Output: 255

        // Checked: returns std::optional (nullopt on overflow)
        const auto checked = safe_num::checked_add(x, y);
        std::cout << "checked_add(250, 10) = "
                  << (checked.has_value() ? "value" : "nullopt") << std::endl;
        // Output: nullopt

        // Overflowing: returns {result, overflowed_flag}
        const auto [val, overflowed] = safe_num::overflowing_add(x, y);
        std::cout << "overflowing_add(250, 10) = {" << val
                  << ", " << (overflowed ? "true" : "false") << "}" << std::endl;
        // Output: {4, true}
    }

    // -----------------------------------------------------------------------
    // 5. Mixed-width arithmetic
    // -----------------------------------------------------------------------
    std::cout << "\n--- Mixed-width arithmetic ---\n";

    // SafeNumbers: mixed-width operations are a compile-time error
    //   safe_num::u8{1U} + safe_num::u32{2U};  // fails with static_assert

    // SafeNumerics: mixed-width operations are allowed with C++ promotion rules
    {
        safe_nrc::safe<std::uint8_t> x = 200;
        safe_nrc::safe<std::uint32_t> y = 100;
        auto result = x + y;  // promoted to safe<uint32_t>
        std::cout << "safe_numerics mixed: u8(200) + u32(100) = "
                  << static_cast<std::uint32_t>(result) << std::endl;
        // Output: 300
    }

    // SafeNumbers requires explicit conversion to the same type first:
    {
        const auto x = safe_num::u8{200U};
        const auto y = safe_num::u32{100U};
        // Convert x to u32 explicitly, then add
        const auto result = safe_num::u32{static_cast<std::uint32_t>(x)} + y;
        std::cout << "safe_numbers explicit: u32(u8(200)) + u32(100) = "
                  << result << std::endl;
        // Output: 300
    }

    // -----------------------------------------------------------------------
    // 6. Mixed safe/built-in arithmetic
    // -----------------------------------------------------------------------
    std::cout << "\n--- Mixed safe/built-in arithmetic ---\n";

    // SafeNumerics: safe<T> can operate directly with built-in types
    {
        safe_nrc::safe<std::uint8_t> x = 200;
        auto result = x + 100;  // int operand is implicitly accepted
        std::cout << "safe_numerics: safe<u8>(200) + 100 = "
                  << static_cast<int>(result) << std::endl;
        // Output: 300
    }

    // SafeNumbers: mixing safe types with built-in types is a compile-time error
    //   safe_num::u8{200U} + 100;          // error: no matching operator+
    //   safe_num::u8{200U} + std::uint8_t{100};  // error: no matching operator+

    // -----------------------------------------------------------------------
    // 7. Unary minus on unsigned types
    // -----------------------------------------------------------------------
    // SafeNumbers: unary minus is a compile-time error via static_assert
    //   auto neg = -safe_num::u8{5U};  // static_assert: "Unary minus is deliberately
    //                             //  disabled for unsigned safe integers"

    // SafeNumerics: unary minus promotes uint8_t to int, so -5 is valid
    std::cout << "\n--- Unary minus on unsigned ---\n";
    try
    {
        safe_nrc::safe<std::uint8_t> x = 5;
        auto result = -x;  // promoted to safe<int>, value is -5
        std::cout << "safe_numerics: -5u = " << static_cast<int>(result) << std::endl;
        // Output: -5 (promoted to int, no error!)
    }
    catch (const std::exception& e)
    {
        std::cout << "safe_numerics threw on unary minus: " << e.what() << std::endl;
    }

    // -----------------------------------------------------------------------
    // 8. Narrowing conversions
    // -----------------------------------------------------------------------
    // Both libraries catch narrowing - this is where SafeNumerics' promotion
    // would eventually trigger an error (when assigning the promoted result
    // back to a narrow type).
    std::cout << "\n--- Narrowing conversions ---\n";

    // SafeNumbers: explicit cast throws std::domain_error if value doesn't fit
    try
    {
        const auto wide = safe_num::u32{300U};
        const auto narrow = static_cast<std::uint8_t>(wide);
        std::cout << "safe_numbers: u32(300) -> u8 = "
                  << static_cast<int>(narrow) << std::endl;
    }
    catch (const std::domain_error& e)
    {
        std::cout << "safe_numbers narrowing threw: " << e.what() << std::endl;
    }

    // SafeNumerics: implicit narrowing throws
    try
    {
        safe_nrc::safe<std::uint32_t> wide = 300;
        safe_nrc::safe<std::uint8_t> narrow = wide;
        std::cout << "safe_numerics implicit: u32(300) -> u8 = "
                  << static_cast<int>(narrow) << std::endl;
    }
    catch (const std::exception& e)
    {
        std::cout << "safe_numerics implicit narrowing threw: " << e.what() << std::endl;
    }

    // SafeNumerics: explicit narrowing via static_cast also throws
    try
    {
        safe_nrc::safe<std::uint32_t> wide = 300;
        auto narrow = static_cast<safe_nrc::safe<std::uint8_t>>(wide);
        std::cout << "safe_numerics explicit: u32(300) -> u8 = "
                  << static_cast<int>(narrow) << std::endl;
    }
    catch (const std::exception& e)
    {
        std::cout << "safe_numerics explicit narrowing threw: " << e.what() << std::endl;
    }

    // -----------------------------------------------------------------------
    // 9. Bounded / range-constrained types
    // -----------------------------------------------------------------------
    // Both libraries provide types constrained to a compile-time range.
    // SafeNumbers: bounded_uint<Min, Max>
    // SafeNumerics: safe_unsigned_range<Min, Max>
    //
    // Critical difference: SafeNumbers enforces bounds on every arithmetic
    // result. SafeNumerics only enforces bounds on construction/assignment
    // back to the range type - the promoted result of arithmetic can exceed
    // the declared range silently.
    std::cout << "\n--- Bounded / range-constrained types ---\n";

    using percent_sn = safe_num::bounded_uint<0U, 100U>;
    using percent_snr = safe_nrc::safe_unsigned_range<0, 100>;

    // Both reject out-of-range construction
    try
    {
        auto x = percent_sn{150U};
        std::cout << "safe_numbers bounded(150): " << x << std::endl;
    }
    catch (const std::domain_error& e)
    {
        std::cout << "safe_numbers bounded(150) threw: " << e.what() << std::endl;
    }

    try
    {
        percent_snr x = 150;
        std::cout << "safe_numerics range(150): " << static_cast<int>(x) << std::endl;
    }
    catch (const std::exception& e)
    {
        std::cout << "safe_numerics range(150) threw: " << e.what() << std::endl;
    }

    // Arithmetic within bounds works for both
    {
        const auto result = percent_sn{60U} + percent_sn{30U};
        std::cout << "safe_numbers bounded: 60 + 30 = " << result << std::endl;
        // Output: 90
    }

    {
        percent_snr a = 60;
        percent_snr b = 30;
        auto result = a + b;
        std::cout << "safe_numerics range: 60 + 30 = " << static_cast<int>(result) << std::endl;
        // Output: 90
    }

    // Arithmetic exceeding the declared range:
    // SafeNumbers catches it immediately, SafeNumerics does not
    try
    {
        percent_sn a {60U};
        percent_sn b {50U};
        auto result = a + b;
        std::cout << "safe_numbers bounded: 60 + 50 = " << result << std::endl;
    }
    catch (const std::exception& e)
    {
        std::cout << "safe_numbers bounded 60+50 threw: " << e.what() << std::endl;
    }

    {
        percent_snr a = 60;
        percent_snr b = 50;
        auto result = a + b;
        std::cout << "safe_numerics range: 60 + 50 = " << static_cast<int>(result) << std::endl;
        // Output: 110 (exceeds declared range of [0,100] but no error!)
    }

    return 0;
}

Output:

--- Construction ---
safe_numbers explicit: u8{200} = 200
safe_numerics implicit: u8 = 200 -> 200
safe_numerics from bool: 1

--- Overflow on addition ---
safe_numbers threw: Overflow detected in unsigned addition
safe_numerics: 256

--- Underflow on subtraction ---
safe_numbers threw: Underflow detected in unsigned subtraction
safe_numerics: -1

--- Alternative policies (SafeNumbers) ---
saturating_add(250, 10) = 255
checked_add(250, 10) = nullopt
overflowing_add(250, 10) = {4, true}

--- Mixed-width arithmetic ---
safe_numerics mixed: u8(200) + u32(100) = 300
safe_numbers explicit: u32(u8(200)) + u32(100) = 300

--- Mixed safe/built-in arithmetic ---
safe_numerics: safe<u8>(200) + 100 = 300

--- Unary minus on unsigned ---
safe_numerics: -5u = -5

--- Narrowing conversions ---
safe_numbers narrowing threw: Overflow in conversion to smaller type
safe_numerics implicit narrowing threw: converted unsigned value too large: positive overflow error
safe_numerics explicit narrowing threw: converted unsigned value too large: positive overflow error

--- Bounded / range-constrained types ---
safe_numbers bounded(150) threw: Construction from value outside the bounds
safe_numerics range(150) threw: converted signed value too large: positive overflow error
safe_numbers bounded: 60 + 30 = 90
safe_numerics range: 60 + 30 = 90
safe_numbers bounded 60+50 threw: Construction from value outside the bounds
safe_numerics range: 60 + 50 = 110