Examples

All the following examples can be found in the examples/ folder of the library.

Basic Construction

C++ has no 128-bit literal, so the first practical question about a 128-bit type is how to get a value into one. This example covers every route: from a built-in integer, from an explicit pair of high and low 64-bit words, from the _u128 and _i128 user-defined literals, from the BOOST_INT128_UINT128_C and BOOST_INT128_INT128_C macros, from a string through a stream, and from floating point. The floating-point cases at the end are worth reading closely, because the conversion is total: NaN yields zero and out-of-range values saturate to the nearest limit rather than invoking undefined behavior.

Example 1. This example demonstrates the various ways to construct uint128 and int128 values
// Copyright 2025 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

#include <boost/int128/int128.hpp>
#include <boost/int128/literals.hpp>
#include <boost/int128/iostream.hpp>
#include <iostream>
#include <limits>
#include <sstream>

int main()
{
    using boost::int128::uint128;
    using boost::int128::int128;

    std::cout << "=== uint128 Construction ===" << std::endl;

    // 1) From a builtin integer type
    constexpr uint128 from_builtin {42U};
    std::cout << "From builtin (42U): " << from_builtin << std::endl;

    // 2) From high and low 64-bit values (high, low)
    constexpr uint128 from_parts {UINT64_C(0x1), UINT64_C(0x0)};  // 2^64
    std::cout << "From parts (1, 0) = 2^64: " << from_parts << std::endl;

    constexpr uint128 max_value {UINT64_C(0xFFFFFFFFFFFFFFFF), UINT64_C(0xFFFFFFFFFFFFFFFF)};
    std::cout << "From parts (max, max): " << max_value << std::endl;
    std::cout << "  Equals numeric_limits max? " << std::boolalpha
              << (max_value == std::numeric_limits<uint128>::max()) << std::endl;

    // 3) From user-defined literals.
    // The library provides only string-form UDLs
    // For small values like this a string is still parsed rather than direct construction
    // Using the constructors for values that fit in (unsigned) long long should be preferred for performance
    using namespace boost::int128::literals;
    const auto small_literal {12345_U128};
    std::cout << "From literal 12345_U128: " << small_literal << std::endl;

    // 4) From macro (like UINT64_C but for 128-bit), good for values that exceed unsigned long long
    const auto from_macro {BOOST_INT128_UINT128_C(340282366920938463463374607431768211455)};
    std::cout << "From BOOST_INT128_UINT128_C(max): " << from_macro << std::endl;

    // 5) From input stream
    std::stringstream ss;
    ss.str("12345678901234567890123456789");
    uint128 from_stream;
    ss >> from_stream;
    std::cout << "From stringstream: " << from_stream << std::endl;

    std::cout << "\n=== int128 Construction ===" << std::endl;

    // Signed from builtin
    constexpr int128 signed_builtin {-42};
    std::cout << "From builtin (-42): " << signed_builtin << std::endl;

    // Signed from parts. Both words are stored unsigned, but the constructor takes
    // the high word signed; read it back with signed_high().
    constexpr int128 min_value {INT64_MIN, 0};
    std::cout << "From parts (INT64_MIN, 0): " << min_value << std::endl;
    std::cout << "  Equals numeric_limits min? "
              << (min_value == std::numeric_limits<int128>::min()) << std::endl;

    // Signed literals. Values that fit in unsigned long long can be written
    // directly; the leading minus is parsed as a unary operator on the
    // literal result (lowercase and uppercase suffixes both work):
    const auto negative_literal {-12345_i128};
    std::cout << "From literal -12345_i128: " << negative_literal << std::endl;

    const auto positive_literal {12345_I128};
    std::cout << "From literal 12345_I128: " << positive_literal << std::endl;

    // For magnitudes beyond unsigned long long you can use the macro or a string literal
    const auto large_signed {BOOST_INT128_INT128_C(-99999999999999999999)};
    std::cout << "From BOOST_INT128_INT128_C(-99999999999999999999): " << large_signed << std::endl;

    const auto large_signed_string {"-99999999999999999999"_i128};
    std::cout << "From string literal: " << large_signed_string << std::endl;

    // Signed macro
    const auto from_signed_macro {BOOST_INT128_INT128_C(-170141183460469231731687303715884105728)};
    std::cout << "From BOOST_INT128_INT128_C(min): " << from_signed_macro << std::endl;

    std::cout << "\n=== Default and Copy Construction ===" << std::endl;

    // Default construction (zero-initialized)
    constexpr uint128 default_constructed {};
    std::cout << "Default constructed: " << default_constructed << std::endl;

    // Copy construction
    const uint128 copied {from_macro};
    std::cout << "Copy constructed: " << copied << std::endl;

    std::cout << "\n=== Floating-Point Construction ===" << std::endl;

    // Floating-point construction truncates toward zero, matching the behavior of
    // a static_cast from a floating-point type to a built-in integer.
    constexpr uint128 from_double {12345.9};
    std::cout << "uint128 from 12345.9 (truncated): " << from_double << std::endl;

    constexpr int128 from_negative_double {-12345.9};
    std::cout << "int128 from -12345.9 (truncated toward zero): " << from_negative_double << std::endl;

    // Values that exceed the 64-bit range are routed through the full 128-bit decomposition.
    const double two_to_the_100 {1.2676506002282294e30};  // 2^100
    const uint128 large_from_double {two_to_the_100};
    std::cout << "uint128 from 2^100: " << large_from_double << std::endl;

    std::cout << "\n=== Floating-Point Edge Cases ===" << std::endl;

    // NaN yields zero for both signed and unsigned (mirrors libgcc's __fix(uns)Xfti).
    const double nan_value {std::numeric_limits<double>::quiet_NaN()};
    const uint128 unsigned_from_nan {nan_value};
    const int128 signed_from_nan {nan_value};
    std::cout << "uint128 from NaN: " << unsigned_from_nan << std::endl;
    std::cout << "int128 from NaN: " << signed_from_nan << std::endl;

    // Negative values are clamped to zero when constructing uint128.
    const uint128 unsigned_from_negative {-1.0};
    std::cout << "uint128 from -1.0 (clamped to zero): " << unsigned_from_negative << std::endl;

    // Positive overflow saturates: anything >= 2^128 (including +infinity) becomes UINT128_MAX.
    const double infinity {std::numeric_limits<double>::infinity()};
    const uint128 saturated_unsigned {infinity};
    std::cout << "uint128 from +infinity (saturates to UINT128_MAX): " << saturated_unsigned << std::endl;

    // For int128, values >= 2^127 saturate to INT128_MAX and values <= -2^127 saturate to INT128_MIN.
    const double huge {1e40};  // Well beyond 2^127 (~ 1.7e38)
    const int128 saturated_positive {huge};
    const int128 saturated_negative {-huge};
    std::cout << "int128 from 1e40 (saturates to INT128_MAX): " << saturated_positive << std::endl;
    std::cout << "int128 from -1e40 (saturates to INT128_MIN): " << saturated_negative << std::endl;

    return 0;
}
Expected Output
=== uint128 Construction ===
From builtin (42U): 42
From parts (1, 0) = 2^64: 18446744073709551616
From parts (max, max): 340282366920938463463374607431768211455
  Equals numeric_limits max? true
From literal 12345_U128: 12345
From BOOST_INT128_UINT128_C(max): 340282366920938463463374607431768211455
From stringstream: 12345678901234567890123456789

=== int128 Construction ===
From builtin (-42): -42
From parts (INT64_MIN, 0): -170141183460469231731687303715884105728
  Equals numeric_limits min? true
From literal -12345_i128: -12345
From literal 12345_I128: 12345
From BOOST_INT128_INT128_C(-99999999999999999999): -99999999999999999999
From string literal: -99999999999999999999
From BOOST_INT128_INT128_C(min): -170141183460469231731687303715884105728

=== Default and Copy Construction ===
Default constructed: 0
Copy constructed: 340282366920938463463374607431768211455

=== Floating-Point Construction ===
uint128 from 12345.9 (truncated): 12345
int128 from -12345.9 (truncated toward zero): -12345
uint128 from 2^100: 1267650600228229401496703205376

=== Floating-Point Edge Cases ===
uint128 from NaN: 0
int128 from NaN: 0
uint128 from -1.0 (clamped to zero): 0
uint128 from +infinity (saturates to UINT128_MAX): 340282366920938463463374607431768211455
int128 from 1e40 (saturates to INT128_MAX): 170141183460469231731687303715884105727
int128 from -1e40 (saturates to INT128_MIN): -170141183460469231731687303715884105728

Basic Arithmetic

The operators are designed so that the types can be dropped into code written for the built-in integers, including the compound assignments and the increment and decrement operators. Note the multiplication in the middle of the output, which is the case that motivates a 128-bit type in the first place: the product of two values near 10^12 has no 64-bit representation.

Example 2. This example demonstrates arithmetic operations with 128-bit integers
// Copyright 2025 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

#include <boost/int128/int128.hpp>
#include <boost/int128/iostream.hpp>
#include <iostream>

int main()
{
    // The types of this library support all arithmetic operations one would expect.
    // They can be between values of the same type, or the same signedness by default.
    // See `mixed_type_arithmetic.cpp` for operations with different signedness.

    using boost::int128::int128;
    using boost::int128::uint128;

    // Basic arithmetic with signed 128-bit integers
    int128 x {1000000000000LL};  // 1 trillion
    int128 y {999999999999LL};   // Just under 1 trillion

    std::cout << "=== Signed 128-bit Arithmetic ===" << std::endl;
    std::cout << "x = " << x << std::endl;
    std::cout << "y = " << y << std::endl;

    // Addition and subtraction
    std::cout << "\nAddition and Subtraction:" << std::endl;
    std::cout << "x + y = " << (x + y) << std::endl;
    std::cout << "x - y = " << (x - y) << std::endl;

    // Multiplication - results that exceed 64-bit range
    std::cout << "\nMultiplication (exceeds 64-bit range):" << std::endl;
    std::cout << "x * y = " << (x * y) << std::endl;

    // Division and modulo
    std::cout << "\nDivision and Modulo:" << std::endl;
    std::cout << "x / 7 = " << (x / 7) << std::endl;
    std::cout << "x % 7 = " << (x % 7) << std::endl;

    // Comparisons
    std::cout << "\nComparisons:" << std::endl;
    std::cout << "x > y:  " << std::boolalpha << (x > y) << std::endl;
    std::cout << "x == y: " << (x == y) << std::endl;
    std::cout << "x != y: " << (x != y) << std::endl;

    // Negative values and absolute value
    int128 negative {-42};
    std::cout << "\nNegative values:" << std::endl;
    std::cout << "negative = " << negative << std::endl;
    std::cout << "abs(negative) = " << boost::int128::abs(negative) << std::endl;

    // Compound assignment operators
    std::cout << "\nCompound assignment operators:" << std::endl;
    int128 z {100};
    std::cout << "z = " << z << std::endl;

    z += 50;
    std::cout << "z += 50: " << z << std::endl;

    z -= 25;
    std::cout << "z -= 25: " << z << std::endl;

    z *= 2;
    std::cout << "z *= 2:  " << z << std::endl;

    z /= 5;
    std::cout << "z /= 5:  " << z << std::endl;

    z %= 7;
    std::cout << "z %= 7:  " << z << std::endl;

    // Unsigned 128-bit arithmetic - useful for very large positive values
    std::cout << "\n=== Unsigned 128-bit Arithmetic ===" << std::endl;
    uint128 large {UINT64_C(0x1), UINT64_C(0x0)};  // 2^64
    std::cout << "large (2^64) = " << large << std::endl;
    std::cout << "large * 2 = " << (large * 2U) << std::endl;
    std::cout << "large + large = " << (large + large) << std::endl;

    // Increment and decrement
    std::cout << "\nIncrement and Decrement:" << std::endl;
    int128 counter {10};
    std::cout << "counter = " << counter << std::endl;
    std::cout << "++counter = " << ++counter << std::endl;
    std::cout << "counter++ = " << counter++ << std::endl;
    std::cout << "counter = " << counter << std::endl;
    std::cout << "--counter = " << --counter << std::endl;

    return 0;
}
Expected Output
=== Signed 128-bit Arithmetic ===
x = 1000000000000
y = 999999999999

Addition and Subtraction:
x + y = 1999999999999
x - y = 1

Multiplication (exceeds 64-bit range):
x * y = 999999999999000000000000

Division and Modulo:
x / 7 = 142857142857
x % 7 = 1

Comparisons:
x > y:  true
x == y: false
x != y: true

Negative values:
negative = -42
abs(negative) = 42

Compound assignment operators:
z = 100
z += 50: 150
z -= 25: 125
z *= 2:  250
z /= 5:  50
z %= 7:  1

=== Unsigned 128-bit Arithmetic ===
large (2^64) = 18446744073709551616
large * 2 = 36893488147419103232
large + large = 36893488147419103232

Increment and Decrement:
counter = 10
++counter = 11
counter++ = 11
counter = 12
--counter = 11

IO Streaming

Neither the built-in __int128 nor the MSVC simulated types have stream support, so printing a 128-bit value on those normally means writing a digit loop by hand. This example shows the insertion and extraction operators, the standard base and case manipulators applying exactly as they do to built-in integers, and a round trip through std::stringstream.

Example 3. This example demonstrates iostream support and string conversions
// Copyright 2025 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

#include <boost/int128/int128.hpp>
#include <boost/int128/iostream.hpp>
#include <iostream>
#include <sstream>

int main()
{
    using boost::int128::int128;
    using boost::int128::uint128;

    std::cout << "=== Basic Streaming ===" << std::endl;

    // Both types allow streaming as one would expect from a regular builtin-type
    constexpr int128 signed_value {-42};
    std::cout << "Signed value: " << signed_value << std::endl;

    // We can also use <iomanip> to change the output format
    constexpr uint128 unsigned_value {0x1, UINT64_MAX};
    std::cout << "Unsigned value (dec): " << unsigned_value << '\n'
              << "Unsigned value (hex): " << std::hex << unsigned_value << '\n'
              << "Unsigned value (oct): " << std::oct << unsigned_value << std::endl;

    // Hex also can be manipulated to be uppercase
    std::cout << "Upper unsigned value: " << std::hex << std::uppercase << unsigned_value << std::endl;

    // And returned to default formatting
    std::cout << "Lower unsigned value: " << std::dec << std::nouppercase << unsigned_value << std::endl;

    // Large values that exceed 64-bit range
    std::cout << "\n=== Large Values (Beyond 64-bit) ===" << std::endl;

    // 2^64 = 18446744073709551616 (first value that doesn't fit in uint64_t)
    constexpr uint128 two_to_64 {1, 0};
    std::cout << "2^64 = " << two_to_64 << std::endl;

    // 2^100 = a very large number
    constexpr uint128 two_to_100 {uint128{1} << 100};
    std::cout << "2^100 = " << two_to_100 << std::endl;

    // Maximum uint128 value
    constexpr auto uint_max {std::numeric_limits<uint128>::max()};
    std::cout << "uint128 max = " << uint_max << std::endl;

    // Minimum and maximum int128 values
    constexpr auto int_min {std::numeric_limits<int128>::min()};
    constexpr auto int_max {std::numeric_limits<int128>::max()};
    std::cout << "int128 min = " << int_min << std::endl;
    std::cout << "int128 max = " << int_max << std::endl;

    // String conversion using stringstream
    std::cout << "\n=== String Conversion with std::stringstream ===" << std::endl;

    // Convert uint128 to string
    std::ostringstream oss;
    oss << two_to_100;
    auto str {oss.str()};
    std::cout << "uint128 to string: \"" << str << "\"" << std::endl;

    // Convert string to uint128
    std::istringstream iss {"123456789012345678901234567890"};
    uint128 parsed_value {};
    iss >> parsed_value;
    std::cout << "String to uint128: " << parsed_value << std::endl;

    // Round-trip: value -> string -> value
    std::cout << "\n=== Round-trip Conversion ===" << std::endl;
    constexpr uint128 original {0xDEADBEEF, 0xCAFEBABE12345678};
    std::ostringstream oss2;
    oss2 << original;
    auto original_str {oss2.str()};

    std::istringstream iss2 {original_str};
    uint128 round_tripped {};
    iss2 >> round_tripped;

    std::cout << "Original:     " << original << std::endl;
    std::cout << "As string:    \"" << original_str << "\"" << std::endl;
    std::cout << "Round-tripped: " << round_tripped << std::endl;
    std::cout << "Match: " << std::boolalpha << (original == round_tripped) << std::endl;

    return 0;
}
Expected Output
=== Basic Streaming ===
Signed value: -42
Unsigned value (dec): 36893488147419103231
Unsigned value (hex): 1ffffffffffffffff
Unsigned value (oct): 3777777777777777777777
Upper unsigned value: 1FFFFFFFFFFFFFFFF
Lower unsigned value: 36893488147419103231

=== Large Values (Beyond 64-bit) ===
2^64 = 18446744073709551616
2^100 = 1267650600228229401496703205376
uint128 max = 340282366920938463463374607431768211455
int128 min = -170141183460469231731687303715884105728
int128 max = 170141183460469231731687303715884105727

=== String Conversion with std::stringstream ===
uint128 to string: "1267650600228229401496703205376"
String to uint128: 123456789012345678901234567890

=== Round-trip Conversion ===
Original:     68915718020162848918556923512
As string:    "68915718020162848918556923512"
Round-tripped: 68915718020162848918556923512
Match: true

Rollover Behavior

Overflow is defined for both types. uint128 wraps modulo 2^128 as any unsigned integer does, and int128 wraps using two’s complement rather than invoking the undefined behavior that built-in signed overflow does; both types report is_modulo through std::numeric_limits. This example steps over each of the four boundaries in turn.

Example 4. This example demonstrates the rollover behavior of both the unsigned and signed type
// Copyright 2025 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt
//
// This example demonstrates the rollover behavior for both signed and unsigned int128

#include <boost/int128.hpp>
#include <iostream>
#include <iomanip>
#include <limits>

int main()
{
    using boost::int128::uint128;
    using boost::int128::int128;

    constexpr uint128 max_unsigned_value {std::numeric_limits<uint128>::max()};
    constexpr uint128 min_unsigned_value {std::numeric_limits<uint128>::min()};

    std::cout << "=== uint128 behavior ===" << std::endl;

    std::cout << "Max of uint128: " << max_unsigned_value << '\n'
              << "Max + 1U: " << max_unsigned_value + 1U << "\n\n";

    std::cout << "Min of uint128: " << min_unsigned_value << '\n'
              << "Min - 1U: " << min_unsigned_value - 1U << "\n\n";

    constexpr int128 max_signed_value {std::numeric_limits<int128>::max()};
    constexpr int128 min_signed_value {std::numeric_limits<int128>::min()};

    std::cout << "=== int128 behavior ===" << std::endl;

    std::cout << "Max of int128: " << max_signed_value << '\n'
              << "Max + 1: " << max_signed_value + 1 << "\n";

    std::cout << "\nMin of int128: " << min_signed_value << '\n'
              << "Min - 1: " << min_signed_value - 1 << '\n' << std::endl;
}
Expected Output
=== uint128 behavior ===
Max of uint128: 340282366920938463463374607431768211455
Max + 1U: 0

Min of uint128: 0
Min - 1U: 340282366920938463463374607431768211455

=== int128 behavior ===
Max of int128: 170141183460469231731687303715884105727
Max + 1: -170141183460469231731687303715884105728

Min of int128: -170141183460469231731687303715884105728
Min - 1: 170141183460469231731687303715884105727

Bitwise Functions (<bit>)

The <bit> functions are provided for uint128, and every one of them is constexpr. This example takes advantage of that by asserting all of its results with static_assert, so the whole thing is checked at compile time: if it builds, it passes. It also shows that bit.hpp can be included on its own, without pulling in the umbrella header.

Example 5. This example demonstrates bitwise operations from the <bit> header
// Copyright 2025 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

// Individual headers

#include <boost/int128/int128.hpp>
#include <boost/int128/bit.hpp>

// Or you can do a single header

// #include <boost/int128.hpp>

int main()
{
    // The functions from bit are only available for uint128

    constexpr boost::int128::uint128 x {1U};

    // All the functions are constexpr

    // Does the value have only a single bit set?
    static_assert(boost::int128::has_single_bit(x), "Should have one bit");

    // How many zeros from the left
    static_assert(boost::int128::countl_zero(x) == 127U, "Should be 127");

    // The bit width of the value
    // 1 + 1 is 10 in binary which is 2 bits wide
    static_assert(boost::int128::bit_width(x + x) == 2U, "2 bits wide");

    // The smallest power of two not greater than the input value
    static_assert(boost::int128::bit_floor(3U * x) == 2U, "2 < 3");

    // The smallest power of two not Smaller than the input value
    static_assert(boost::int128::bit_ceil(5U * x) == 8U, "8 > 5");

    // How many zeros from the right?
    static_assert(boost::int128::countr_zero(2U * x) == 1, "1 zero to the right of 10");

    // How many 1-bits in the value
    static_assert(boost::int128::popcount(7U * x) == 3, "111");

    // Swap the bytes
    // Create a value with distinct byte pattern
    constexpr boost::int128::uint128 original{
        0x0123456789ABCDEFULL,
        0xFEDCBA9876543210ULL
    };

    // Expected result after byteswap
    constexpr boost::int128::uint128 expected{
        0x1032547698BADCFEULL,
        0xEFCDAB8967452301ULL
    };

    static_assert(boost::int128::byteswap(original) == expected, "Mismatched byteswap");
    static_assert(boost::int128::byteswap(expected) == original, "Mismatched byteswap");

    return 0;
}

Saturating Arithmetic (<numeric>)

Saturating operations clamp at the representable boundary instead of wrapping, which is what signal processing, counters, and accumulators generally want: a value pinned at the maximum is far less damaging than one that has silently wrapped to near zero. This example covers addition, subtraction, multiplication, and division, along with saturating_cast for narrowing to a smaller type. The division case is the subtle one, since int_min / -1 is the only signed division whose true result is not representable.

Example 6. This example demonstrates saturating arithmetic operations that clamp to min/max instead of overflowing
// Copyright 2025 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

// Individual headers

#include <boost/int128/int128.hpp>
#include <boost/int128/numeric.hpp>
#include <boost/int128/iostream.hpp>

// Or you can do a single header

// #include <boost/int128.hpp>

#include <limits>
#include <type_traits>
#include <iostream>

int main()
{
    using boost::int128::uint128;
    using boost::int128::int128;

    // std::numeric_limits is overloaded for both types
    constexpr auto uint_max {std::numeric_limits<uint128>::max()};
    static_assert(std::is_same<decltype(uint_max), const uint128>::value, "Types should match");

    constexpr auto int_max {std::numeric_limits<int128>::max()};
    constexpr auto int_min {std::numeric_limits<int128>::min()};

    std::cout << "=== Saturating Arithmetic ===" << std::endl;
    std::cout << "uint128 max = " << uint_max << std::endl;
    std::cout << "int128 max  = " << int_max << std::endl;
    std::cout << "int128 min  = " << int_min << std::endl;

    // Saturating arithmetic returns max on overflow, or min on underflow rather than rolling over
    std::cout << "\n=== Saturating Addition and Subtraction ===" << std::endl;
    std::cout << "saturating_add(uint_max, uint_max) = " << boost::int128::saturating_add(uint_max, uint_max)
              << " (saturates to uint_max)" << std::endl;
    std::cout << "saturating_sub(0, uint_max) = " << boost::int128::saturating_sub(uint128{0}, uint_max)
              << " (saturates to 0, not underflow)" << std::endl;

    // This is especially useful for signed types since rollover is undefined
    std::cout << "\n=== Saturating Multiplication ===" << std::endl;
    std::cout << "saturating_mul(int_max, 2) = " << boost::int128::saturating_mul(int_max, 2)
              << " (saturates to int_max)" << std::endl;
    std::cout << "saturating_mul(-(int_max - 2), 5) = " << boost::int128::saturating_mul(-(int_max - 2), 5)
              << " (saturates to int_min)" << std::endl;

    // The only case in the library where saturating_div overflows is x = int_min and y = -1
    std::cout << "\n=== Saturating Division ===" << std::endl;
    std::cout << "saturating_div(int_min, -1) = " << boost::int128::saturating_div(int_min, -1)
              << " (saturates to int_max; normally this overflows)" << std::endl;

    // saturating_cast allows types to be safely converted without rollover behavior
    std::cout << "\n=== Saturating Casts ===" << std::endl;
    std::cout << "saturating_cast<int128>(uint_max) = " << boost::int128::saturating_cast<int128>(uint_max)
              << " (saturates to int_max)" << std::endl;

    // You can also cast to builtin types
    std::cout << "saturating_cast<int64_t>(int_max) = " << boost::int128::saturating_cast<std::int64_t>(int_max)
              << " (saturates to INT64_MAX)" << std::endl;

    // Even of different signedness as this is treated like a static cast
    std::cout << "saturating_cast<int32_t>(uint_max) = " << boost::int128::saturating_cast<std::int32_t>(uint_max)
              << " (saturates to INT32_MAX)" << std::endl;

    return 0;
}
Expected Output
=== Saturating Arithmetic ===
uint128 max = 340282366920938463463374607431768211455
int128 max  = 170141183460469231731687303715884105727
int128 min  = -170141183460469231731687303715884105728

=== Saturating Addition and Subtraction ===
saturating_add(uint_max, uint_max) = 340282366920938463463374607431768211455 (saturates to uint_max)
saturating_sub(0, uint_max) = 0 (saturates to 0, not underflow)

=== Saturating Multiplication ===
saturating_mul(int_max, 2) = 170141183460469231731687303715884105727 (saturates to int_max)
saturating_mul(-(int_max - 2), 5) = -170141183460469231731687303715884105728 (saturates to int_min)

=== Saturating Division ===
saturating_div(int_min, -1) = 170141183460469231731687303715884105727 (saturates to int_max; normally this overflows)

=== Saturating Casts ===
saturating_cast<int128>(uint_max) = 170141183460469231731687303715884105727 (saturates to int_max)
saturating_cast<int64_t>(int_max) = 9223372036854775807 (saturates to INT64_MAX)
saturating_cast<int32_t>(uint_max) = 2147483647 (saturates to INT32_MAX)

Numeric Algorithms (<numeric>)

gcd, lcm, and midpoint mirror their <numeric> counterparts for the 128-bit types. The midpoint section repays a close look: the result is computed without ever forming a + b, so it stays correct for operands near the maximum where the obvious (a + b) / 2 would overflow.

Example 7. This example demonstrates gcd, lcm, and midpoint algorithms
// Copyright 2025 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

#include <boost/int128/int128.hpp>
#include <boost/int128/numeric.hpp>
#include <boost/int128/iostream.hpp>
#include <iostream>

int main()
{
    using boost::int128::uint128;
    using boost::int128::int128;

    std::cout << "=== Greatest Common Divisor (gcd) ===" << std::endl;

    // Basic gcd
    constexpr uint128 a {48};
    constexpr uint128 b {18};
    std::cout << "gcd(" << a << ", " << b << ") = " << boost::int128::gcd(a, b) << std::endl;

    // gcd with larger values
    constexpr uint128 large_a {123456789012345678ULL};
    constexpr uint128 large_b {987654321098765432ULL};
    std::cout << "gcd(" << large_a << ", " << large_b << ") = "
              << boost::int128::gcd(large_a, large_b) << std::endl;

    // gcd with 128-bit values
    constexpr uint128 huge_a {uint128{1} << 100};
    constexpr uint128 huge_b {uint128{1} << 80};
    std::cout << "gcd(2^100, 2^80) = " << boost::int128::gcd(huge_a, huge_b) << " (= 2^80)" << std::endl;

    // Signed gcd (always returns positive)
    constexpr int128 neg_a {-48};
    constexpr int128 neg_b {18};
    std::cout << "gcd(" << neg_a << ", " << neg_b << ") = "
              << boost::int128::gcd(neg_a, neg_b) << " (always positive)" << std::endl;

    std::cout << "\n=== Least Common Multiple (lcm) ===" << std::endl;

    // Basic lcm
    constexpr uint128 x {12};
    constexpr uint128 y {18};
    std::cout << "lcm(" << x << ", " << y << ") = " << boost::int128::lcm(x, y) << std::endl;

    // lcm with coprime numbers
    constexpr uint128 p {7};
    constexpr uint128 q {11};
    std::cout << "lcm(" << p << ", " << q << ") = " << boost::int128::lcm(p, q)
              << " (coprime: lcm = p * q)" << std::endl;

    // Relationship: gcd(a,b) * lcm(a,b) = a * b
    std::cout << "\nVerifying gcd * lcm = a * b:" << std::endl;
    auto g {boost::int128::gcd(x, y)};
    auto l {boost::int128::lcm(x, y)};
    std::cout << "gcd(" << x << ", " << y << ") * lcm(" << x << ", " << y << ") = "
              << (g * l) << std::endl;
    std::cout << x << " * " << y << " = " << (x * y) << std::endl;

    std::cout << "\n=== Midpoint ===" << std::endl;

    // Unsigned midpoint
    constexpr uint128 low {10};
    constexpr uint128 high {20};
    std::cout << "midpoint(" << low << ", " << high << ") = "
              << boost::int128::midpoint(low, high) << std::endl;

    // Midpoint with odd sum (rounds toward first argument)
    constexpr uint128 odd_low {10};
    constexpr uint128 odd_high {21};
    std::cout << "midpoint(" << odd_low << ", " << odd_high << ") = "
              << boost::int128::midpoint(odd_low, odd_high) << " (rounds toward first arg)" << std::endl;
    std::cout << "midpoint(" << odd_high << ", " << odd_low << ") = "
              << boost::int128::midpoint(odd_high, odd_low) << " (rounds toward first arg)" << std::endl;

    // Midpoint avoids overflow (unlike (a+b)/2)
    std::cout << "\n--- Overflow-safe midpoint ---" << std::endl;
    constexpr auto uint_max {std::numeric_limits<uint128>::max()};
    constexpr auto uint_max_minus_10 {uint_max - 10U};
    std::cout << "midpoint(uint128_max, uint128_max - 10) = "
              << boost::int128::midpoint(uint_max, uint_max_minus_10) << std::endl;
    std::cout << "(This would overflow if computed as (a + b) / 2)" << std::endl;

    // Signed midpoint
    std::cout << "\n--- Signed midpoint ---" << std::endl;
    constexpr int128 neg {-100};
    constexpr int128 pos {100};
    std::cout << "midpoint(" << neg << ", " << pos << ") = "
              << boost::int128::midpoint(neg, pos) << std::endl;

    constexpr int128 neg2 {-100};
    constexpr int128 neg3 {-50};
    std::cout << "midpoint(" << neg2 << ", " << neg3 << ") = "
              << boost::int128::midpoint(neg2, neg3) << std::endl;

    return 0;
}
Expected Output
=== Greatest Common Divisor (gcd) ===
gcd(48, 18) = 6
gcd(123456789012345678, 987654321098765432) = 2
gcd(2^100, 2^80) = 1208925819614629174706176 (= 2^80)
gcd(-48, 18) = 6 (always positive)

=== Least Common Multiple (lcm) ===
lcm(12, 18) = 36
lcm(7, 11) = 77 (coprime: lcm = p * q)

Verifying gcd * lcm = a * b:
gcd(12, 18) * lcm(12, 18) = 216
12 * 18 = 216

=== Midpoint ===
midpoint(10, 20) = 15
midpoint(10, 21) = 15 (rounds toward first arg)
midpoint(21, 10) = 16 (rounds toward first arg)

--- Overflow-safe midpoint ---
midpoint(uint128_max, uint128_max - 10) = 340282366920938463463374607431768211450
(This would overflow if computed as (a + b) / 2)

--- Signed midpoint ---
midpoint(-100, 100) = 0
midpoint(-100, -50) = -75

Integer Division (<numeric>)

The div_* family divides with every rounding mode other than the truncation operator/ performs. Two sections repay a close look: rem_euclid is the only remainder that is never negative, which is what makes it usable for wrapping an offset into a range, and div_to_pos_inf computes a ceiling without the (length + block - 1) / block idiom that wraps near the top of the range.

Example 8. This example demonstrates the rounding modes, tie breaking, and the combined quotient and remainder functions
// Copyright 2026 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

#include <boost/int128/int128.hpp>
#include <boost/int128/numeric.hpp>
#include <boost/int128/iostream.hpp>
#include <iostream>
#include <limits>

// tag::exclude[]
// Only present with MSVC 14.1
#ifdef _MSC_VER
#  pragma warning(push)
#  pragma warning(disable : 4307) // integral constant overflow
#  pragma warning(disable : 4146) // unary minus operator applied to unsigned type, result still unsigned
#endif
// end::exclude[]

int main()
{
    using boost::int128::uint128;
    using boost::int128::int128;

    std::cout << "=== Every rounding mode on -12 / 5 ===" << std::endl;

    // The exact quotient is -2.4, so the nearest integer is -2 and no mode ties
    constexpr int128 x {-12};
    constexpr int128 y {5};

    std::cout << "div_to_zero        = " << boost::int128::div_to_zero(x, y) << std::endl;
    std::cout << "div_away_zero      = " << boost::int128::div_away_zero(x, y) << std::endl;
    std::cout << "div_to_pos_inf     = " << boost::int128::div_to_pos_inf(x, y) << std::endl;
    std::cout << "div_to_neg_inf     = " << boost::int128::div_to_neg_inf(x, y) << std::endl;
    std::cout << "div_euclid         = " << boost::int128::div_euclid(x, y) << std::endl;
    std::cout << "div_ties_to_zero   = " << boost::int128::div_ties_to_zero(x, y) << std::endl;

    std::cout << "\n=== Tie breaking on -7 / 2 ===" << std::endl;

    // The exact quotient is -3.5, so every tie-breaking rule picks a different side
    constexpr int128 tie_x {-7};
    constexpr int128 tie_y {2};

    std::cout << "div_ties_to_zero     = " << boost::int128::div_ties_to_zero(tie_x, tie_y) << std::endl;
    std::cout << "div_ties_away_zero   = " << boost::int128::div_ties_away_zero(tie_x, tie_y) << std::endl;
    std::cout << "div_ties_to_pos_inf  = " << boost::int128::div_ties_to_pos_inf(tie_x, tie_y) << std::endl;
    std::cout << "div_ties_to_neg_inf  = " << boost::int128::div_ties_to_neg_inf(tie_x, tie_y) << std::endl;
    std::cout << "div_ties_to_odd      = " << boost::int128::div_ties_to_odd(tie_x, tie_y) << std::endl;
    std::cout << "div_ties_to_even     = " << boost::int128::div_ties_to_even(tie_x, tie_y) << std::endl;

    std::cout << "\n=== Quotient and remainder from one division ===" << std::endl;

    // Each div_rem_ function performs a single division and returns both halves
    constexpr auto floored {boost::int128::div_rem_to_neg_inf(x, y)};
    std::cout << "div_rem_to_neg_inf(-12, 5): quotient = " << floored.quotient
              << ", remainder = " << floored.remainder << std::endl;

    constexpr auto truncated {boost::int128::div_rem_to_zero(x, y)};
    std::cout << "div_rem_to_zero(-12, 5):    quotient = " << truncated.quotient
              << ", remainder = " << truncated.remainder << std::endl;

    // The remainder always satisfies x == quotient * y + remainder
    std::cout << "quotient * y + remainder = " << (floored.quotient * y + floored.remainder) << std::endl;

    std::cout << "\n=== Euclidean remainder is never negative ===" << std::endl;

    // operator% takes its sign from the dividend, which makes it a poor fit for
    // wrapping an offset into a range. rem_euclid always lands in [0, abs(y)).
    constexpr int128 modulus {7};
    for (int128 offset {-9}; offset <= -6; ++offset)
    {
        std::cout << offset << " % 7 = " << (offset % modulus)
                  << ", rem_euclid(" << offset << ", 7) = " << boost::int128::rem_euclid(offset, modulus) << std::endl;
    }

    std::cout << "\n=== Ceiling division without overflow ===" << std::endl;

    // Counting fixed size blocks needed to cover a length is the classic use for
    // rounding towards positive infinity. The usual (length + block - 1) / block
    // overflows here, while div_to_pos_inf does not.
    constexpr auto length {(std::numeric_limits<uint128>::max)()};
    constexpr uint128 block {1000};

    std::cout << "length = " << length << std::endl;
    std::cout << "div_to_pos_inf(length, 1000) = " << boost::int128::div_to_pos_inf(length, block) << std::endl;
    std::cout << "div_to_zero(length, 1000)    = " << boost::int128::div_to_zero(length, block) << std::endl;
    std::cout << "(length + 999) / 1000        = " << ((length + 999U) / block) << " (wrapped)" << std::endl;

    std::cout << "\n=== Unbiased rounding of a scaled value ===" << std::endl;

    // Rounding half to even keeps a long running sum from drifting upwards, which is
    // what operator/ plus a manual half-adjustment would do
    constexpr int128 scale {1000};
    const int128 samples[] {int128{1500}, int128{2500}, int128{3500}, int128{-1500}};

    for (const auto sample : samples)
    {
        std::cout << sample << " / 1000: ties_to_even = " << boost::int128::div_ties_to_even(sample, scale)
                  << ", ties_away_zero = " << boost::int128::div_ties_away_zero(sample, scale) << std::endl;
    }

    return 0;
}
Expected Output
=== Every rounding mode on -12 / 5 ===
div_to_zero        = -2
div_away_zero      = -3
div_to_pos_inf     = -2
div_to_neg_inf     = -3
div_euclid         = -3
div_ties_to_zero   = -2

=== Tie breaking on -7 / 2 ===
div_ties_to_zero     = -3
div_ties_away_zero   = -4
div_ties_to_pos_inf  = -3
div_ties_to_neg_inf  = -4
div_ties_to_odd      = -3
div_ties_to_even     = -4

=== Quotient and remainder from one division ===
div_rem_to_neg_inf(-12, 5): quotient = -3, remainder = 3
div_rem_to_zero(-12, 5):    quotient = -2, remainder = -2
quotient * y + remainder = -12

=== Euclidean remainder is never negative ===
-9 % 7 = -2, rem_euclid(-9, 7) = 5
-8 % 7 = -1, rem_euclid(-8, 7) = 6
-7 % 7 = 0, rem_euclid(-7, 7) = 0
-6 % 7 = -6, rem_euclid(-6, 7) = 1

=== Ceiling division without overflow ===
length = 340282366920938463463374607431768211455
div_to_pos_inf(length, 1000) = 340282366920938463463374607431768212
div_to_zero(length, 1000)    = 340282366920938463463374607431768211
(length + 999) / 1000        = 0 (wrapped)

=== Unbiased rounding of a scaled value ===
1500 / 1000: ties_to_even = 2, ties_away_zero = 2
2500 / 1000: ties_to_even = 2, ties_away_zero = 3
3500 / 1000: ties_to_even = 4, ties_away_zero = 4
-1500 / 1000: ties_to_even = -2, ties_away_zero = -2

Checked Arithmetic

ckd_add, ckd_sub, and ckd_mul follow the C23 checked-integer contract: each returns true when the operation overflowed, and always stores the wrapped result through the out pointer, so a caller can branch on the flag and still inspect what the wrap produced. The mixed-type section at the end is the part worth studying, because it is the requested result type rather than the operand types that determines whether an operation overflowed.

Example 9. This example demonstrates checked addition, subtraction, and multiplication following the C23 checked-integer contract
// Copyright 2026 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

// Individual headers

#include <boost/int128/utilities.hpp>
#include <boost/int128/iostream.hpp>

// Or you can do a single header

// #include <boost/int128.hpp>

#include <cstdint>
#include <limits>
#include <iostream>

int main()
{
    using boost::int128::uint128;
    using boost::int128::int128;
    using boost::int128::ckd_add;
    using boost::int128::ckd_sub;
    using boost::int128::ckd_mul;

    std::cout << std::boolalpha;

    // ckd_add, ckd_sub, and ckd_mul implement the C23 stdckdint.h contract: the
    // operation is evaluated as if both operands had infinite range, the result
    // is written to *result wrapped to that type's width, and the function
    // returns true when the exact result did not fit.
    constexpr auto u_max {std::numeric_limits<uint128>::max()};
    constexpr auto i_max {std::numeric_limits<int128>::max()};
    constexpr auto i_min {std::numeric_limits<int128>::min()};

    // A result that fits returns false and holds the exact value.
    std::cout << "=== Results That Fit ===" << std::endl;
    int128 r {};
    bool overflow {ckd_add(&r, int128{20}, int128{22})};
    std::cout << "ckd_add(20, 22): overflow=" << overflow << ", result=" << r << std::endl;

    // Addition that exceeds the type wraps modulo 2^128 and reports overflow.
    std::cout << "\n=== Addition Overflow ===" << std::endl;
    uint128 u {};
    overflow = ckd_add(&u, u_max, uint128{1});
    std::cout << "ckd_add(UINT128_MAX, 1): overflow=" << overflow << ", wrapped=" << u << std::endl;

    // Subtracting below zero in an unsigned type wraps to the top of the range.
    std::cout << "\n=== Subtraction Underflow ===" << std::endl;
    overflow = ckd_sub(&u, uint128{0}, uint128{1});
    std::cout << "ckd_sub(0, 1): overflow=" << overflow << ", wrapped=" << u << std::endl;

    // Multiplication detects overflow that operator* would silently roll over,
    // including INT128_MIN * -1, whose true result is not representable.
    std::cout << "\n=== Multiplication Overflow ===" << std::endl;
    overflow = ckd_mul(&r, i_max, int128{2});
    std::cout << "ckd_mul(INT128_MAX, 2): overflow=" << overflow << ", wrapped=" << r << std::endl;
    overflow = ckd_mul(&r, i_min, int128{-1});
    std::cout << "ckd_mul(INT128_MIN, -1): overflow=" << overflow << ", wrapped=" << r << std::endl;

    // The result type and the two operand types are independent: they may differ
    // in width and signedness, and the exact mathematical value is always used.
    std::cout << "\n=== Mixed Types ===" << std::endl;
    std::int64_t narrow {};
    overflow = ckd_add(&narrow, uint128{5}, int128{-3});
    std::cout << "ckd_add<int64_t>(uint128{5}, int128{-3}): overflow=" << overflow
              << ", result=" << narrow << std::endl;

    // Narrow targets make the wrap-around easy to see (400 modulo 256 is 144).
    std::uint8_t byte {};
    overflow = ckd_mul(&byte, std::uint8_t{20}, std::uint8_t{20});
    std::cout << "ckd_mul<uint8_t>(20, 20): overflow=" << overflow
              << ", wrapped=" << static_cast<int>(byte) << std::endl;

    return 0;
}
Expected Output
=== Results That Fit ===
ckd_add(20, 22): overflow=false, result=42

=== Addition Overflow ===
ckd_add(UINT128_MAX, 1): overflow=true, wrapped=0

=== Subtraction Underflow ===
ckd_sub(0, 1): overflow=true, wrapped=340282366920938463463374607431768211455

=== Multiplication Overflow ===
ckd_mul(INT128_MAX, 2): overflow=true, wrapped=-2
ckd_mul(INT128_MIN, -1): overflow=true, wrapped=-170141183460469231731687303715884105728

=== Mixed Types ===
ckd_add<int64_t>(uint128{5}, int128{-3}): overflow=false, result=2
ckd_mul<uint8_t>(20, 20): overflow=true, wrapped=144

Mixed Signedness Arithmetic

128-bit values rarely appear on their own; in practice they get added to loop counters and compared against small constants. Every arithmetic, bitwise, comparison, and shift operator is therefore defined between the 128-bit types and any built-in integer type, signed or unsigned, following the usual arithmetic conversions. See Mixed Type Operations for the full set of conversion rules that this example exercises.

Example 10. This example demonstrates arithmetic between 128-bit and built-in integer types
// Copyright 2025 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

#include <boost/int128.hpp>
#include <iostream>

int main()
{
    // Mixed-sign comparisons and arithmetic between int128, uint128, and
    // built-in integer types of opposite signedness follow the C++ usual
    // arithmetic conversions, identical to the built-in __int128 /
    // unsigned __int128 types.

    std::cout << "=== Mixed Type Arithmetic with uint128 ===" << std::endl;

    constexpr boost::int128::uint128 unsigned_value {3};
    std::cout << "unsigned_value = " << unsigned_value << std::endl;

    constexpr auto greater_unsigned_value {unsigned_value + 5};

    std::cout << "unsigned_value + 1 = " << (unsigned_value + 1) << std::endl;
    std::cout << "unsigned_value - 1 = " << (unsigned_value - 1) << std::endl;
    std::cout << "unsigned_value * 2 = " << (unsigned_value * 2) << std::endl;
    std::cout << "unsigned_value / 3 = " << (unsigned_value / 3) << std::endl;
    std::cout << "unsigned_value % 3 = " << (unsigned_value % 3) << std::endl;
    std::cout << "unsigned_value + 5 = " << (unsigned_value + 5)
              << " (same as greater_unsigned_value: " << greater_unsigned_value << ")" << std::endl;

    std::cout << "\n=== Mixed Type Arithmetic with int128 ===" << std::endl;

    constexpr boost::int128::int128 signed_value {-3};
    std::cout << "signed_value = " << signed_value << std::endl;

    std::cout << "signed_value + 1U = " << (signed_value + 1U) << std::endl;
    std::cout << "signed_value - 4U = " << (signed_value - 4U) << std::endl;
    std::cout << "signed_value * 2 = " << (signed_value * 2) << std::endl;
    std::cout << "signed_value / 4U = " << (signed_value / 4U) << std::endl;

    return 0;
}
Expected Output
=== Mixed Type Arithmetic with uint128 ===
unsigned_value = 3
unsigned_value + 1 = 4
unsigned_value - 1 = 2
unsigned_value * 2 = 6
unsigned_value / 3 = 1
unsigned_value % 3 = 0
unsigned_value + 5 = 8 (same as greater_unsigned_value: 8)

=== Mixed Type Arithmetic with int128 ===
signed_value = -3
signed_value + 1U = -2
signed_value - 4U = -7
signed_value * 2 = -6
signed_value / 4U = 0

Boost.Math and Boost.Random Integration

Because the types report the traits that generic code expects of an integer, they can serve as the value type of other libraries instead of only appearing in hand-written arithmetic. This example draws 128-bit values from a Boost.Random distribution and passes them through Boost.Math statistical functions. The printed values differ from run to run, so the block below is illustrative rather than an exact expectation.

Example 11. This example demonstrates integration with Boost.Math and Boost.Random libraries
// Copyright 2025 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

#include <boost/int128.hpp>
#include <boost/int128/random.hpp> // Not included in the convenience header, but needed for boost.random interop

#include <boost/math/statistics/univariate_statistics.hpp>
#include <boost/random/uniform_int_distribution.hpp>
#include <iostream>
#include <limits>
#include <array>
#include <random>

int main()
{
    std::cout << "=== uint128 ===" << '\n';

    // Setup our rng and distribution
    std::mt19937_64 rng {42};
    boost::random::uniform_int_distribution<boost::int128::uint128> dist {0, (std::numeric_limits<boost::int128::uint128>::max)()};

    // Create a dataset for ourselves of random uint128s using our dist and rng from above
    std::array<boost::int128::uint128, 10000> data_set;
    for (auto& value : data_set)
    {
        value = dist(rng);
    }

    // Perform some rudimentary statistical analysis on our dataset
    std::cout << "    Mean: " << boost::math::statistics::mean(data_set) << '\n';
    std::cout << "Variance: " << boost::math::statistics::variance(data_set) << '\n';
    std::cout << "  Median: " << boost::math::statistics::median(data_set) << '\n';

    std::cout << "=== int128 ===" << '\n';

    // We can also generate random signed integers using int128
    boost::random::uniform_int_distribution<boost::int128::int128> signed_dist {std::numeric_limits<boost::int128::int128>::min(), std::numeric_limits<boost::int128::int128>::max()};

    std::cout << "Random int128: " << signed_dist(rng) << std::endl;

    return 0;
}
Example Output (values vary per run)
=== uint128 ===
    Mean: 22125900135088040520646253247977468
Variance: 15183108029620265677746188314852225
  Median: 169775281866460752209725324063124732284
=== int128 ===
Random int128: 45422201008201503618595888886744218664

Boost.Charconv Integration

to_chars and from_chars are the fastest conversion route the library offers, and the only one that is entirely locale-independent, which makes them the right choice for serialization formats and wire protocols. Including <boost/int128/charconv.hpp> is all that is needed: the overloads are injected into Boost.Charconv itself. This example round-trips the extreme value of each type and shows base 16 and base 8 conversion.

Example 12. This example demonstrates fast string-to-number and number-to-string conversions using Boost.Charconv
// Copyright 2025 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

#include <boost/int128/int128.hpp>
#include <boost/int128/iostream.hpp>
#include <boost/int128/charconv.hpp>
#include <boost/charconv.hpp>
#include <iostream>
#include <limits>
#include <cstring>

int main()
{
    using boost::int128::uint128;
    using boost::int128::int128;

    char buffer[64];

    // === to_chars: Convert integers to character strings ===
    std::cout << "=== to_chars ===" << std::endl;

    // Unsigned 128-bit to decimal string
    constexpr uint128 max_u128 {std::numeric_limits<uint128>::max()};
    auto result {boost::charconv::to_chars(buffer, buffer + sizeof(buffer), max_u128)};
    *result.ptr = '\0';
    std::cout << "uint128 max (decimal): " << buffer << std::endl;

    // Signed 128-bit to decimal string
    constexpr int128 min_i128 {std::numeric_limits<int128>::min()};
    result = boost::charconv::to_chars(buffer, buffer + sizeof(buffer), min_i128);
    *result.ptr = '\0';
    std::cout << "int128 min (decimal):  " << buffer << std::endl;

    // Hexadecimal output (base 16)
    uint128 hex_value {UINT64_C(0xDEADBEEF), UINT64_C(0xCAFEBABE12345678)};
    result = boost::charconv::to_chars(buffer, buffer + sizeof(buffer), hex_value, 16);
    *result.ptr = '\0';
    std::cout << "uint128 (hex): 0x" << buffer << std::endl;

    // Octal output (base 8)
    result = boost::charconv::to_chars(buffer, buffer + sizeof(buffer), int128{511}, 8);
    *result.ptr = '\0';
    std::cout << "int128 511 (octal): 0" << buffer << std::endl;

    // === from_chars: Parse character strings to integers ===
    std::cout << "\n=== from_chars ===" << std::endl;

    // Parse decimal string to uint128
    const char* decimal_str {"340282366920938463463374607431768211455"};
    uint128 parsed_unsigned;
    boost::charconv::from_chars(decimal_str, decimal_str + std::strlen(decimal_str), parsed_unsigned);
    std::cout << "Parsed \"" << decimal_str << "\"" << std::endl;
    std::cout << "  Result: " << parsed_unsigned << std::endl;
    std::cout << "  Equals max? " << std::boolalpha << (parsed_unsigned == max_u128) << std::endl;

    // Parse negative decimal string to int128
    const char* negative_str {"-170141183460469231731687303715884105728"};
    int128 parsed_signed;
    boost::charconv::from_chars(negative_str, negative_str + std::strlen(negative_str), parsed_signed);
    std::cout << "Parsed \"" << negative_str << "\"" << std::endl;
    std::cout << "  Result: " << parsed_signed << std::endl;
    std::cout << "  Equals min? " << (parsed_signed == min_i128) << std::endl;

    // Parse hexadecimal string (base 16)
    const char* hex_str {"DEADBEEFCAFEBABE12345678"};
    uint128 parsed_hex;
    boost::charconv::from_chars(hex_str, hex_str + std::strlen(hex_str), parsed_hex, 16);
    std::cout << "Parsed hex \"" << hex_str << "\"" << std::endl;
    std::cout << "  Result: " << parsed_hex << std::endl;

    return 0;
}
Expected Output
=== to_chars ===
uint128 max (decimal): 340282366920938463463374607431768211455
int128 min (decimal):  -170141183460469231731687303715884105728
uint128 (hex): 0xdeadbeefcafebabe12345678
int128 511 (octal): 0777

=== from_chars ===
Parsed "340282366920938463463374607431768211455"
  Result: 340282366920938463463374607431768211455
  Equals max? true
Parsed "-170141183460469231731687303715884105728"
  Result: -170141183460469231731687303715884105728
  Equals min? true
Parsed hex "DEADBEEFCAFEBABE12345678"
  Result: 68915718020162848918556923512

Boost.ContainerHash Integration

One of the most common uses of a 128-bit integer is as a key, whether a UUID, an IPv6 address, or a content hash, so hashing the types well matters. This example confirms that boost::hash and std::hash agree for both types, builds a composite key from a pair of 128-bit coordinates with boost::hash_combine, and uses the types as keys in both boost::unordered_map and the open-addressing boost::unordered_flat_map.

Example 13. This example demonstrates using the library types with Boost.ContainerHash: the boost::hash functor, boost::hash_combine for composite keys, and the boost::unordered containers that default to boost::hash
// Copyright 2026 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

// This example demonstrates Boost.ContainerHash integration with int128 types.
// Including <boost/int128/hash.hpp> injects hash_value overloads for the library
// types, so boost::hash, boost::hash_combine, boost::hash_range, and the
// boost::unordered containers (which default to boost::hash) all work with no
// extra configuration.

#include <boost/int128/int128.hpp>
#include <boost/int128/hash.hpp>
#include <boost/container_hash/hash.hpp>
#include <boost/unordered/unordered_map.hpp>
#include <boost/unordered/unordered_flat_map.hpp>
#include <cstddef>
#include <functional>
#include <iostream>
#include <string>

using boost::int128::uint128;
using boost::int128::int128;

// A user-defined composite key that holds 128-bit fields. Providing a hash_value
// overload in the type's own namespace lets Boost.ContainerHash find it via ADL,
// and boost::hash_combine reuses the int128 hashes supplied by hash.hpp.
struct point
{
    int128 x;
    int128 y;
};

bool operator==(const point& lhs, const point& rhs)
{
    return lhs.x == rhs.x && lhs.y == rhs.y;
}

std::size_t hash_value(const point& p)
{
    std::size_t seed {0};
    boost::hash_combine(seed, p.x);
    boost::hash_combine(seed, p.y);
    return seed;
}

int main()
{
    std::cout << "=== boost::hash on 128-bit types ===" << std::endl;

    // boost::hash<T> dispatches to the hash_value overloads from hash.hpp, which
    // delegate to std::hash, so the two functors always agree.
    const uint128 big {UINT64_C(0xDEADBEEF), UINT64_C(0xCAFEBABE12345678)};
    const int128 neg {-123456789012345678LL};

    std::cout << "boost::hash matches std::hash (uint128): "
              << std::boolalpha << (boost::hash<uint128>{}(big) == std::hash<uint128>{}(big)) << std::endl;
    std::cout << "boost::hash matches std::hash (int128):  "
              << (boost::hash<int128>{}(neg) == std::hash<int128>{}(neg)) << std::endl;

    std::cout << "\n=== boost::unordered_map<uint128, ...> ===" << std::endl;

    // boost::unordered_map defaults to boost::hash<Key>, so uint128 keys need
    // no explicit hasher.
    boost::unordered_map<uint128, std::string> labels {};
    labels[uint128{1, 0}] = "two to the sixty-fourth";                   // 2^64
    labels[uint128{UINT64_C(0x8000000000000000), 0}] = "two to the one hundred twenty-seventh"; // 2^127
    labels[uint128{42}] = "forty-two";

    std::cout << "Entries: " << labels.size() << std::endl;
    std::cout << "Label at 2^64: " << labels[uint128{1, 0}] << std::endl;
    std::cout << "Contains 42: " << (labels.find(uint128{42}) != labels.end()) << std::endl;

    std::cout << "\n=== hash_combine for a composite key ===" << std::endl;

    // The point hasher combines two int128 fields; boost::hash<point> finds it
    // via ADL, letting point be used as a key directly.
    boost::unordered_map<point, long> populations {};
    populations[point{10, 20}] = 5000000;
    populations[point{-30, 40}] = 250000;

    std::cout << "Cities stored: " << populations.size() << std::endl;
    std::cout << "Population at (10, 20): " << populations[point{10, 20}] << std::endl;
    std::cout << "Same coordinate hashes equal: "
              << (boost::hash<point>{}(point{10, 20}) == boost::hash<point>{}(point{10, 20})) << std::endl;

    std::cout << "\n=== boost::unordered_flat_map<int128, ...> ===" << std::endl;

    // The modern flat container also defaults to boost::hash.
    boost::unordered_flat_map<int128, int> counts {};
    counts[int128{-1}] = 1;
    counts[int128{0}] = 2;
    counts[int128{1}] = 3;

    std::cout << "Flat map size: " << counts.size() << std::endl;
    std::cout << "counts[-1] = " << counts[int128{-1}] << std::endl;

    return 0;
}
Expected Output
=== boost::hash on 128-bit types ===
boost::hash matches std::hash (uint128): true
boost::hash matches std::hash (int128):  true

=== boost::unordered_map<uint128, ...> ===
Entries: 3
Label at 2^64: two to the sixty-fourth
Contains 42: true

=== hash_combine for a composite key ===
Cities stored: 2
Population at (10, 20): 5000000
Same coordinate hashes equal: true

=== boost::unordered_flat_map<int128, ...> ===
Flat map size: 3
counts[-1] = 1

String Conversion (to_string)

to_string is the convenience route to a std::string, mirroring std::to_string for the built-in integers. This example checks that agreement directly for values inside the 64-bit range and then continues past it. The last case is a deliberate trap: the magnitude of the most negative int128 is larger than the maximum int128, so it cannot be written as a negated literal. The NOTE following the output explains what to use instead.

Example 14. This example demonstrates to_string for 128-bit integers, comparing results against std::to_string for values that fit in 64 bits
// Copyright 2025 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

#include <boost/int128/int128.hpp>
#include <boost/int128/string.hpp>
#include <boost/int128/literals.hpp>
#include <iostream>
#include <string>
#include <cstdint>

int main()
{
    using boost::int128::uint128;
    using boost::int128::int128;
    using boost::int128::to_string;
    using namespace boost::int128::literals;

    std::cout << "=== to_string with uint128 ===" << std::endl;

    // Compare against std::to_string for values that fit in 64 bits
    constexpr uint128 u_small {UINT64_C(1234567890)};
    const auto u_small_str {to_string(u_small)};
    const auto u_small_std {std::to_string(std::uint64_t{1234567890})};
    std::cout << "uint128 to_string(1234567890): " << u_small_str << std::endl;
    std::cout << "std::to_string(uint64_t 1234567890): " << u_small_std << std::endl;
    std::cout << "Match: " << std::boolalpha << (u_small_str == u_small_std) << std::endl;

    constexpr uint128 u_max64 {UINT64_MAX};
    const auto u_max64_str {to_string(u_max64)};
    const auto u_max64_std {std::to_string(UINT64_MAX)};
    std::cout << "\nuint128 to_string(UINT64_MAX): " << u_max64_str << std::endl;
    std::cout << "std::to_string(UINT64_MAX):      " << u_max64_std << std::endl;
    std::cout << "Match: " << (u_max64_str == u_max64_std) << std::endl;

    // Values beyond 64-bit range
    const auto large_unsigned {340282366920938463463374607431768211455_U128};
    std::cout << "\nuint128 max: " << to_string(large_unsigned) << std::endl;

    std::cout << "\n=== to_string with int128 ===" << std::endl;

    // Compare against std::to_string for values that fit in 64 bits
    constexpr int128 s_negative {-42};
    const auto s_neg_str {to_string(s_negative)};
    const auto s_neg_std {std::to_string(std::int64_t{-42})};
    std::cout << "int128 to_string(-42): " << s_neg_str << std::endl;
    std::cout << "std::to_string(int64_t -42): " << s_neg_std << std::endl;
    std::cout << "Match: " << (s_neg_str == s_neg_std) << std::endl;

    constexpr int128 s_large {INT64_MAX};
    const auto s_large_str {to_string(s_large)};
    const auto s_large_std {std::to_string(INT64_MAX)};
    std::cout << "\nint128 to_string(INT64_MAX): " << s_large_str << std::endl;
    std::cout << "std::to_string(INT64_MAX):     " << s_large_std << std::endl;
    std::cout << "Match: " << (s_large_str == s_large_std) << std::endl;

    // Values beyond 64-bit range.
    // INT128_MIN via a negated numeric literal fails: the sign is not part of the
    // literal, so the positive magnitude 2^127 is read first and rejected as out of
    // range. A string literal keeps the sign with the digits, so it parses cleanly.
    const auto large_negative {"-170141183460469231731687303715884105728"_i128};
    std::cout << "\nint128 min with string literal: " << to_string(large_negative) << std::endl;

    const auto large_negative_c {BOOST_INT128_INT128_C(-170141183460469231731687303715884105728)};
    std::cout << "\nint128 min with INT128_C macro: " << to_string(large_negative_c) << std::endl;

    const auto large_positive {std::numeric_limits<int128>::max()};
    std::cout << "int128 max: " << to_string(large_positive) << std::endl;

    return 0;
}
Expected Output
=== to_string with uint128 ===
uint128 to_string(1234567890): 1234567890
std::to_string(uint64_t 1234567890): 1234567890
Match: true

uint128 to_string(UINT64_MAX): 18446744073709551615
std::to_string(UINT64_MAX):      18446744073709551615
Match: true

uint128 max: 340282366920938463463374607431768211455

=== to_string with int128 ===
int128 to_string(-42): -42
std::to_string(int64_t -42): -42
Match: true

int128 to_string(INT64_MAX): 9223372036854775807
std::to_string(INT64_MAX):     9223372036854775807
Match: true

int128 min with string literal: 0

int128 min with INT128_C macro: -170141183460469231731687303715884105728
int128 max: 170141183460469231731687303715884105727
The line int128 min with string literal: 0 is expected. The positive magnitude of the minimum int128 value (2^127) is larger than the maximum int128 value, so it cannot be written as a negated _i128 literal. Construct it with BOOST_INT128_INT128_C or (std::numeric_limits<int128>::min)(), as the example shows.

{fmt} Library Integration

The formatter implements the full standard format specification for the 128-bit types: fill and alignment, the three sign options, the # alternate form, zero padding, width, and all four bases in either case. This example is the exhaustive demonstration of those specifiers. Because the specifier handling is shared between the two backends, everything shown here applies unchanged to std::format after including <boost/int128/format.hpp> and replacing fmt:: with std::.

Example 15. This example demonstrates formatting with the {fmt} library including alignment specifiers
// Copyright 2025 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

// This example demonstrates {fmt} library integration with int128 types.
// Requires {fmt} to be installed: https://github.com/fmtlib/fmt
//
// For C++20 std::format support, use <boost/int128/format.hpp> instead,
// which provides the same formatting capabilities with std::format.

#include <boost/int128/int128.hpp>
#include <boost/int128/fmt_format.hpp>

#include <fmt/format.h>
#include <iostream>

int main()
{
    using boost::int128::int128;
    using boost::int128::uint128;

    std::cout << "=== Basic Formatting ===" << std::endl;

    constexpr uint128 unsigned_value {0xDEADBEEF, 0xCAFEBABE12345678};
    constexpr int128 signed_value {-123456789012345678};

    // Default decimal formatting
    std::cout << fmt::format("Default (decimal): {}", unsigned_value) << std::endl;
    std::cout << fmt::format("Signed value: {}", signed_value) << std::endl;

    std::cout << "\n=== Base Specifiers ===" << std::endl;

    // Different bases: binary, octal, decimal, hex
    constexpr uint128 value {255};
    std::cout << fmt::format("Binary:      {:b}", value) << std::endl;
    std::cout << fmt::format("Octal:       {:o}", value) << std::endl;
    std::cout << fmt::format("Decimal:     {:d}", value) << std::endl;
    std::cout << fmt::format("Hexadecimal: {:x}", value) << std::endl;
    std::cout << fmt::format("Hex (upper): {:X}", value) << std::endl;

    std::cout << "\n=== Alternate Form (Prefixes) ===" << std::endl;

    // Using # for alternate form adds base prefixes
    std::cout << fmt::format("Binary with prefix:  {:#b}", value) << std::endl;
    std::cout << fmt::format("Octal with prefix:   {:#o}", value) << std::endl;
    std::cout << fmt::format("Hex with prefix:     {:#x}", value) << std::endl;
    std::cout << fmt::format("Hex upper prefix:    {:#X}", value) << std::endl;

    std::cout << "\n=== Sign Options ===" << std::endl;

    constexpr int128 positive {42};
    constexpr int128 negative {-42};

    // Sign specifiers: + (always show), - (default), space (space for positive)
    std::cout << fmt::format("Plus sign:  {:+} and {:+}", positive, negative) << std::endl;
    std::cout << fmt::format("Minus only: {} and {}", positive, negative) << std::endl;
    std::cout << fmt::format("Space sign: {: } and {: }", positive, negative) << std::endl;

    std::cout << "\n=== Zero Padding ===" << std::endl;

    // Padding with zeros (no alignment specifier)
    std::cout << fmt::format("8-digit padding:  {:08}", value) << std::endl;
    std::cout << fmt::format("16-digit padding: {:016}", value) << std::endl;

    std::cout << "\n=== Alignment ===" << std::endl;

    // Left, right, and center alignment with default fill (space)
    std::cout << fmt::format("Left align:   '{:<10}'", positive) << std::endl;
    std::cout << fmt::format("Right align:  '{:>10}'", positive) << std::endl;
    std::cout << fmt::format("Center align: '{:^10}'", positive) << std::endl;

    std::cout << "\n=== Alignment with Fill Characters ===" << std::endl;

    // Custom fill characters
    std::cout << fmt::format("Left with *:   '{:*<10}'", positive) << std::endl;
    std::cout << fmt::format("Right with 0:  '{:0>10}'", positive) << std::endl;
    std::cout << fmt::format("Center with -: '{:-^10}'", positive) << std::endl;

    std::cout << "\n=== Alignment with Sign ===" << std::endl;

    // Alignment combined with sign specifiers
    std::cout << fmt::format("Right align +:  '{:>+10}'", positive) << std::endl;
    std::cout << fmt::format("Left align +:   '{:<+10}'", positive) << std::endl;
    std::cout << fmt::format("Center align +: '{:^+11}'", positive) << std::endl;
    std::cout << fmt::format("Right align -:  '{:*>10}'", negative) << std::endl;

    std::cout << "\n=== Alignment with Hex and Prefix ===" << std::endl;

    // Alignment with base specifiers and prefixes
    std::cout << fmt::format("Right align hex:  '{:>10x}'", value) << std::endl;
    std::cout << fmt::format("Left align hex:   '{:<10x}'", value) << std::endl;
    std::cout << fmt::format("Center with prefix: '{:*^#12x}'", value) << std::endl;

    std::cout << "\n=== Large Values ===" << std::endl;

    // Demonstrate with values beyond 64-bit range
    constexpr auto uint_max {std::numeric_limits<uint128>::max()};
    constexpr auto int_min {std::numeric_limits<int128>::min()};

    std::cout << fmt::format("uint128 max: {}", uint_max) << std::endl;
    std::cout << fmt::format("uint128 max (hex): {:#x}", uint_max) << std::endl;
    std::cout << fmt::format("int128 min: {}", int_min) << std::endl;

    std::cout << "\n=== Combined Format Specifiers ===" << std::endl;

    // Combining multiple specifiers
    std::cout << fmt::format("Hex with prefix, uppercase, padded: {:#016X}", unsigned_value) << std::endl;
    std::cout << fmt::format("Signed with plus, padded: {:+020}", signed_value) << std::endl;

    return 0;
}
Expected Output
=== Basic Formatting ===
Default (decimal): 68915718020162848918556923512
Signed value: -123456789012345678

=== Base Specifiers ===
Binary:      11111111
Octal:       377
Decimal:     255
Hexadecimal: ff
Hex (upper): FF

=== Alternate Form (Prefixes) ===
Binary with prefix:  0b11111111
Octal with prefix:   0377
Hex with prefix:     0xff
Hex upper prefix:    0XFF

=== Sign Options ===
Plus sign:  +42 and -42
Minus only: 42 and -42
Space sign:  42 and -42

=== Zero Padding ===
8-digit padding:  00000255
16-digit padding: 0000000000000255

=== Alignment ===
Left align:   '42        '
Right align:  '        42'
Center align: '    42    '

=== Alignment with Fill Characters ===
Left with *:   '42********'
Right with 0:  '0000000042'
Center with -: '----42----'

=== Alignment with Sign ===
Right align +:  '       +42'
Left align +:   '+42       '
Center align +: '    +42    '
Right align -:  '*******-42'

=== Alignment with Hex and Prefix ===
Right align hex:  '        ff'
Left align hex:   'ff        '
Center with prefix: '****0xff****'

=== Large Values ===
uint128 max: 340282366920938463463374607431768211455
uint128 max (hex): 0xffffffffffffffffffffffffffffffff
int128 min: -170141183460469231731687303715884105728

=== Combined Format Specifiers ===
Hex with prefix, uppercase, padded: 0XDEADBEEFCAFEBABE12345678
Signed with plus, padded: -00123456789012345678

Division with Remainder (<cstdlib>)

Division is by far the most expensive 128-bit operation, and it naturally produces the quotient and the remainder together, so writing a / b and a % b separately can cost close to twice what it needs to. div returns both from a single operation, as its <cstdlib> counterpart does. The example also tabulates all four sign combinations, which is worth checking against your expectations: division truncates toward zero, so the remainder takes the sign of the dividend.

Example 16. This example demonstrates the div() function that returns both quotient and remainder
// Copyright 2025 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

#include <boost/int128/int128.hpp>
#include <boost/int128/cstdlib.hpp>
#include <boost/int128/iostream.hpp>
#include <iostream>

int main()
{
    using boost::int128::uint128;
    using boost::int128::int128;

    std::cout << "=== div() Function ===" << std::endl;
    std::cout << "Returns both quotient and remainder in a single operation" << std::endl;

    // Unsigned division
    std::cout << "\n--- Unsigned Division ---" << std::endl;

    constexpr uint128 dividend {1000000000000000000ULL};
    constexpr uint128 divisor {7};

    auto uresult {boost::int128::div(dividend, divisor)};
    std::cout << dividend << " / " << divisor << " = " << uresult.quot
              << " remainder " << uresult.rem << std::endl;

    // Verify: quot * divisor + rem == dividend
    std::cout << "Verification: " << uresult.quot << " * " << divisor
              << " + " << uresult.rem << " = " << (uresult.quot * divisor + uresult.rem) << std::endl;

    // Large value division
    std::cout << "\n--- Large Value Division ---" << std::endl;

    constexpr uint128 large_dividend {uint128{1} << 100};  // 2^100
    constexpr uint128 large_divisor {uint128{1} << 50};    // 2^50

    auto large_result {boost::int128::div(large_dividend, large_divisor)};
    std::cout << "2^100 / 2^50 = " << large_result.quot
              << " remainder " << large_result.rem << std::endl;

    // Signed division
    std::cout << "\n--- Signed Division ---" << std::endl;

    constexpr int128 signed_dividend {-100};
    constexpr int128 signed_divisor {7};

    auto sresult {boost::int128::div(signed_dividend, signed_divisor)};
    std::cout << signed_dividend << " / " << signed_divisor << " = " << sresult.quot
              << " remainder " << sresult.rem << std::endl;

    // Different sign combinations
    std::cout << "\n--- Sign Combinations ---" << std::endl;

    constexpr int128 pos {17};
    constexpr int128 neg {-17};
    constexpr int128 div_pos {5};
    constexpr int128 div_neg {-5};

    auto pp {boost::int128::div(pos, div_pos)};
    auto pn {boost::int128::div(pos, div_neg)};
    auto np {boost::int128::div(neg, div_pos)};
    auto nn {boost::int128::div(neg, div_neg)};

    std::cout << " 17 /  5 = " << pp.quot << " remainder " << pp.rem << std::endl;
    std::cout << " 17 / -5 = " << pn.quot << " remainder " << pn.rem << std::endl;
    std::cout << "-17 /  5 = " << np.quot << " remainder " << np.rem << std::endl;
    std::cout << "-17 / -5 = " << nn.quot << " remainder " << nn.rem << std::endl;

    // Edge case: dividend smaller than divisor
    std::cout << "\n--- Edge Cases ---" << std::endl;

    auto small_div {boost::int128::div(uint128{3}, uint128{10})};
    std::cout << "3 / 10 = " << small_div.quot << " remainder " << small_div.rem << std::endl;

    return 0;
}
Expected Output
=== div() Function ===
Returns both quotient and remainder in a single operation

--- Unsigned Division ---
1000000000000000000 / 7 = 142857142857142857 remainder 1
Verification: 142857142857142857 * 7 + 1 = 1000000000000000000

--- Large Value Division ---
2^100 / 2^50 = 1125899906842624 remainder 0

--- Signed Division ---
-100 / 7 = -14 remainder -2

--- Sign Combinations ---
 17 /  5 = 3 remainder 2
 17 / -5 = -3 remainder 2
-17 /  5 = -3 remainder -2
-17 / -5 = 3 remainder -2

--- Edge Cases ---
3 / 10 = 0 remainder 3

CUDA Usage

The types and most of the free functions are annotated to run on the device, so a kernel can use them the same way host code does. This example is the minimal shape of a device workload and a good starting template: allocate managed memory so one array serves both host and device, compute one gcd per thread, then recompute on the host and compare every element.

Example 17. This example demonstrates how to use library types and functions inside a CUDA kernel.
//  Copyright Matt Borland 2026.
//  Use, modification and distribution are subject to the
//  Boost Software License, Version 1.0. (See accompanying file
//  LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)

#include <iostream>
#include <vector>
#include <random>
#include <limits>
#include <boost/int128.hpp>
#include <boost/int128/numeric.hpp>
#include <boost/int128/random.hpp>
#include <boost/random/uniform_int_distribution.hpp>

#include <cuda_runtime.h>

using test_type = boost::int128::uint128;

// Calculates the GCD of 2 values on device
__global__ void cuda_gcd(const test_type* in1, const test_type* in2, test_type* out, int numElements)
{
    int i = blockDim.x * blockIdx.x + threadIdx.x;

    if (i < numElements)
    {
        out[i] = boost::int128::gcd(in1[i], in2[i]);
    }
}

// Allocate managed space so that the arrays can be used on both host and device
void allocate(test_type** in, int numElements)
{
    cudaError_t err = cudaSuccess;
    err = cudaMallocManaged(in, numElements * sizeof(test_type));
    if (err != cudaSuccess)
    {
        throw std::runtime_error(cudaGetErrorString(err));
    }

    cudaDeviceSynchronize();
}

void cleanup(test_type** in1, test_type** in2, test_type** out)
{
    if (*in1 != nullptr)
    {
        cudaFree(*in1);
        *in1 = nullptr;
    }

    if (*in2 != nullptr)
    {
        cudaFree(*in2);
        *in2 = nullptr;
    }

    if (*out != nullptr)
    {
        cudaFree(*out);
        *out = nullptr;
    }

    cudaDeviceReset();
}

int main()
{
    std::mt19937_64 rng {42};

    const int numElements = 50000;
    std::cout << "[Vector operation on " << numElements << " elements]" << std::endl;

    // Allocate managed space for our inputs and GPU outputs
    // We then fill them with random numbers

    test_type* in1 = nullptr;
    test_type* in2 = nullptr;
    test_type* out = nullptr;

    allocate(&in1, numElements);
    allocate(&in2, numElements);
    allocate(&out, numElements);

    boost::random::uniform_int_distribution<test_type> dist {(std::numeric_limits<test_type>::min)(), (std::numeric_limits<test_type>::max)()};
    for (std::size_t i = 0; i < numElements; ++i)
    {
        in1[i] = dist(rng);
        in2[i] = dist(rng);
    }

    const int threadsPerBlock = 256;
    const int blocksPerGrid = (numElements + threadsPerBlock - 1) / threadsPerBlock;
    std::cout << "CUDA kernel launch with " << blocksPerGrid << " blocks of " << threadsPerBlock << " threads" << std::endl;

    // Launch the CUDA kernel and check for errors

    cuda_gcd<<<blocksPerGrid, threadsPerBlock>>>(in1, in2, out, numElements);
    cudaDeviceSynchronize();

    cudaError_t err = cudaGetLastError();
    if (err != cudaSuccess)
    {
        std::cerr << "Failed to launch kernel (error code " << cudaGetErrorString(err) << ")!" << std::endl;
        cleanup(&in1, &in2, &out);
        return EXIT_FAILURE;
    }

    // We now will perform the same operation using the same inputs on CPU,
    // to compare the results for equality

    std::vector<test_type> results;
    results.reserve(numElements);

    for (int i = 0; i < numElements; ++i)
    {
        results.emplace_back(boost::int128::gcd(in1[i], in2[i]));
    }

    // We can now compare that our operation on GPU and the same operation on CPU have identical results

    for (int i = 0; i < numElements; ++i)
    {
        if (out[i] != results[i])
        {
            std::cerr << "Result verification failed at element: " << i << "!" << std::endl;
            cleanup(&in1, &in2, &out);
            return EXIT_FAILURE;
        }
    }

    cleanup(&in1, &in2, &out);

    std::cout << "All CPU and GPU computed elements match!" << std::endl;

    return 0;
}
Expected Output
[Vector operation on 50000 elements]
CUDA kernel launch with 196 blocks of 256 threads
All CPU and GPU computed elements match!

SYCL Usage

The same workload as the CUDA example above, expressed in SYCL with unified shared memory in place of managed allocations. The one difference in setup is that SYCL device support is opt-in rather than detected, so both the include order and the macro noted below are required.

Example 18. This example demonstrates how to use library types and functions inside a SYCL kernel. Define BOOST_INT128_ENABLE_SYCL and include <sycl/sycl.hpp> before any Boost.int128 header, then compile with icpx -fsycl.
//  Copyright Matt Borland 2026.
//  Use, modification and distribution are subject to the
//  Boost Software License, Version 1.0. (See accompanying file
//  LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)

#include <sycl/sycl.hpp>
#include <boost/int128.hpp>
#include <boost/int128/numeric.hpp>
#include <iostream>
#include <vector>
#include <random>
#include <cstdint>
#include <cstdlib>

using test_type = boost::int128::uint128;

// Calculates the GCD of two values on the SYCL device and verifies against the host
int main()
{
    std::mt19937_64 rng {42};

    const int numElements {50000};
    std::cout << "[Vector operation on " << numElements << " elements]" << std::endl;

    sycl::queue q;
    std::cout << "SYCL device: " << q.get_device().get_info<sycl::info::device::name>() << std::endl;

    // Allocate shared (USM) memory so the arrays are usable on both host and device
    test_type* in1 {sycl::malloc_shared<test_type>(numElements, q)};
    test_type* in2 {sycl::malloc_shared<test_type>(numElements, q)};
    test_type* out {sycl::malloc_shared<test_type>(numElements, q)};

    for (int i {0}; i < numElements; ++i)
    {
        in1[i] = test_type{rng(), rng()};
        in2[i] = test_type{rng(), rng()};
    }

    // Launch the SYCL kernel: each work item computes one gcd
    q.submit([&](sycl::handler& h)
    {
        h.parallel_for(sycl::range<1>(numElements), [=](sycl::id<1> idx)
        {
            const int i {static_cast<int>(idx[0])};
            out[i] = boost::int128::gcd(in1[i], in2[i]);
        });
    }).wait();

    // Perform the same operation on the host and compare
    std::vector<test_type> results;
    results.reserve(numElements);
    for (int i {0}; i < numElements; ++i)
    {
        results.emplace_back(boost::int128::gcd(in1[i], in2[i]));
    }

    int ret {EXIT_SUCCESS};
    for (int i {0}; i < numElements; ++i)
    {
        if (out[i] != results[i])
        {
            std::cerr << "Result verification failed at element: " << i << "!" << std::endl;
            ret = EXIT_FAILURE;
            break;
        }
    }

    if (ret == EXIT_SUCCESS)
    {
        std::cout << "All CPU and GPU computed elements match!" << std::endl;
    }

    sycl::free(in1, q);
    sycl::free(in2, q);
    sycl::free(out, q);

    return ret;
}
Expected Output
[Vector operation on 50000 elements]
SYCL device: Intel(R) ...
All CPU and GPU computed elements match!

CUDA: Primality Testing with 128-bit Intermediates

A GPU is a natural fit for primality testing because every candidate is independent, but the arithmetic is not expressible in 64 bits. Miller-Rabin repeatedly reduces a product of two residues modulo n; when n is a 64-bit value both operands can approach 2^64, so the product needs a full 128 bits before it can be reduced. uint128 supplies that intermediate in device code, where unsigned __int128 is not portably available.

Example 19. This example runs a deterministic Miller-Rabin test over the largest odd values representable in 64 bits, one candidate per thread, using powm and the 128-bit product for the modular squarings.
//  Copyright Matt Borland 2026.
//  Use, modification and distribution are subject to the
//  Boost Software License, Version 1.0. (See accompanying file
//  LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)

// Deterministic Miller-Rabin primality testing on the GPU.
//
// Every step of the test reduces a product of two residues modulo n. For a
// 64-bit n both operands can approach 2^64, so the product needs a full 128
// bits before it can be reduced. uint128 supplies that intermediate in
// device code, where unsigned __int128 is not portably available.

#include <boost/int128.hpp>
#include <cstdint>
#include <iostream>
#include <limits>
#include <stdexcept>
#include <vector>

#include <cuda_runtime.h>

using boost::int128::uint128;

// Number of bases that make Miller-Rabin deterministic for every 64-bit n
constexpr int num_bases {12};

// Computes (a * b) % n for 64-bit operands. The 128-bit product is formed
// first so that no high-order bits are discarded before the reduction.
__host__ __device__ std::uint64_t mulmod(const std::uint64_t a, const std::uint64_t b, const std::uint64_t n) noexcept
{
    return static_cast<std::uint64_t>((uint128{a} * uint128{b}) % uint128{n});
}

// Deterministic Miller-Rabin. Exact for the whole 64-bit range.
__host__ __device__ bool is_prime(const std::uint64_t n) noexcept
{
    const std::uint64_t bases[num_bases] {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37};

    for (int i {0}; i < num_bases; ++i)
    {
        if (n == bases[i])
        {
            return true;
        }

        if (n % bases[i] == 0U)
        {
            return false;
        }
    }

    if (n < 41U)
    {
        return false;
    }

    // Write n - 1 as d * 2^s with d odd
    std::uint64_t d {n - 1U};
    int s {0};
    while ((d & 1U) == 0U)
    {
        d >>= 1U;
        ++s;
    }

    for (int i {0}; i < num_bases; ++i)
    {
        // powm keeps every intermediate in a uint128, so a^d mod n is exact
        auto x {static_cast<std::uint64_t>(boost::int128::powm(uint128{bases[i]}, uint128{d}, uint128{n}))};

        if (x == 1U || x == n - 1U)
        {
            continue;
        }

        bool composite {true};
        for (int r {1}; r < s; ++r)
        {
            x = mulmod(x, x, n);

            if (x == n - 1U)
            {
                composite = false;
                break;
            }
        }

        if (composite)
        {
            return false;
        }
    }

    return true;
}

// One candidate per thread. Writes the candidate back when it is prime, 0 otherwise.
__global__ void cuda_find_primes(const std::uint64_t first, std::uint64_t* out, const int numCandidates)
{
    const auto i {static_cast<int>(blockDim.x * blockIdx.x + threadIdx.x)};

    if (i < numCandidates)
    {
        const std::uint64_t candidate {first + 2ULL * static_cast<std::uint64_t>(i)};
        out[i] = is_prime(candidate) ? candidate : 0ULL;
    }
}

// Allocate managed space so that the array can be used on both host and device
void allocate(std::uint64_t** out, int numElements)
{
    const cudaError_t err {cudaMallocManaged(out, numElements * sizeof(std::uint64_t))};
    if (err != cudaSuccess)
    {
        throw std::runtime_error(cudaGetErrorString(err));
    }

    cudaDeviceSynchronize();
}

void cleanup(std::uint64_t** out)
{
    if (*out != nullptr)
    {
        cudaFree(*out);
        *out = nullptr;
    }

    cudaDeviceReset();
}

int main()
{
    // A 64-bit multiply is not enough, even though both operands are below n
    const std::uint64_t a {11400714819323198485ULL};
    const std::uint64_t b {14029467366897019727ULL};
    const std::uint64_t n {18446744073709551557ULL};

    std::cout << "Full 128-bit product a * b: " << uint128{a} * uint128{b} << std::endl;
    std::cout << "  (a * b) % n, 64-bit math: " << (a * b) % n << "  <- the product wrapped" << std::endl;
    std::cout << "  (a * b) % n via uint128: " << mulmod(a, b, n) << std::endl;
    std::cout << std::endl;

    // Test the largest odd values that fit in 64 bits
    const int numCandidates {1 << 16};
    const std::uint64_t last {(std::numeric_limits<std::uint64_t>::max)()};
    const std::uint64_t first {last - 2ULL * static_cast<std::uint64_t>(numCandidates - 1)};

    std::cout << "[Miller-Rabin on " << numCandidates << " odd candidates from " << first << " to " << last << "]" << std::endl;

    std::uint64_t* out {nullptr};
    allocate(&out, numCandidates);

    const int threadsPerBlock {256};
    const int blocksPerGrid {(numCandidates + threadsPerBlock - 1) / threadsPerBlock};
    std::cout << "CUDA kernel launch with " << blocksPerGrid << " blocks of " << threadsPerBlock << " threads" << std::endl;

    cuda_find_primes<<<blocksPerGrid, threadsPerBlock>>>(first, out, numCandidates);
    cudaDeviceSynchronize();

    const cudaError_t err {cudaGetLastError()};
    if (err != cudaSuccess)
    {
        std::cerr << "Failed to launch kernel (error code " << cudaGetErrorString(err) << ")!" << std::endl;
        cleanup(&out);
        return EXIT_FAILURE;
    }

    // Run the identical test on the host for comparison
    std::vector<std::uint64_t> results;
    results.reserve(numCandidates);

    for (int i {0}; i < numCandidates; ++i)
    {
        const std::uint64_t candidate {first + 2ULL * static_cast<std::uint64_t>(i)};
        results.emplace_back(is_prime(candidate) ? candidate : 0ULL);
    }

    for (int i {0}; i < numCandidates; ++i)
    {
        if (out[i] != results[i])
        {
            std::cerr << "Result verification failed at element: " << i << "!" << std::endl;
            cleanup(&out);
            return EXIT_FAILURE;
        }
    }

    int found {0};
    std::uint64_t largest {0};
    for (int i {0}; i < numCandidates; ++i)
    {
        if (out[i] != 0U)
        {
            ++found;
            largest = out[i];
        }
    }

    std::cout << "Primes found on device: " << found << std::endl;
    std::cout << "Largest prime below 2^64: " << largest << std::endl;

    cleanup(&out);

    std::cout << "All CPU and GPU computed elements match!" << std::endl;

    return 0;
}
Expected Output
Full 128-bit product a * b: 159945956516994065446882290339781513595
  (a * b) % n, 64-bit math: 17693923505768731003  <- the product wrapped
  (a * b) % n via uint128: 12755662373092413040

[Miller-Rabin on 65536 odd candidates from 18446744073709420545 to 18446744073709551615]
CUDA kernel launch with 256 blocks of 256 threads
Primes found on device: 2879
Largest prime below 2^64: 18446744073709551557
All CPU and GPU computed elements match!

SYCL: Exact and Reproducible Reductions

Reductions are the other common reason to reach for a wider integer on a GPU. A dot product of 64-bit integers overflows a 64-bit accumulator, and widening to double only trades a wrong answer for a lossy one. Floating point also makes the result depend on the order of summation, which a parallel reduction does not control: the partitioning of the work becomes part of the answer. Integer addition is associative, so a uint128 accumulator is both exact and independent of how the reduction is split across the device.

Example 20. This example computes an exact dot product on the device as 1024 strided partial sums, then shows that it is bit-for-bit equal to the host result summed in either direction, while a 64-bit accumulator wraps and a double cannot represent even a single product.
//  Copyright Matt Borland 2026.
//  Use, modification and distribution are subject to the
//  Boost Software License, Version 1.0. (See accompanying file
//  LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)

// Exact, order-independent reduction on the GPU.
//
// A dot product of 64-bit integers overflows a 64-bit accumulator, and a
// floating-point accumulator loses the low-order bits. That second problem is
// worse on a GPU than on a CPU: a parallel reduction visits the elements in
// whatever order the hardware schedules, and floating-point addition is not
// associative, so the answer changes from run to run and from device to
// device. A uint128 accumulator is wide enough to be exact here, and
// integer addition is associative, so the device result is bit-for-bit equal
// to the host result no matter how the work is partitioned.

#include <sycl/sycl.hpp>
#include <boost/int128.hpp>
#include <cstdint>
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <random>

using boost::int128::uint128;

int main()
{
    const int numElements {1 << 20};
    const int numPartials {1024};

    std::cout << "[Exact dot product of " << numElements << " pairs of 40-bit integers]" << std::endl;

    sycl::queue q;
    std::cout << "SYCL device: " << q.get_device().get_info<sycl::info::device::name>() << std::endl;

    // Shared (USM) allocations are readable from both the host and the device
    std::uint64_t* lhs {sycl::malloc_shared<std::uint64_t>(numElements, q)};
    std::uint64_t* rhs {sycl::malloc_shared<std::uint64_t>(numElements, q)};
    uint128* partials {sycl::malloc_shared<uint128>(numPartials, q)};

    std::mt19937_64 rng {42};
    for (int i {0}; i < numElements; ++i)
    {
        lhs[i] = rng() >> 24U;
        rhs[i] = rng() >> 24U;
    }

    // Each work item accumulates a strided slice into its own 128-bit partial sum
    q.submit([&](sycl::handler& h)
    {
        h.parallel_for(sycl::range<1>(numPartials), [=](sycl::id<1> idx)
        {
            const int p {static_cast<int>(idx[0])};

            uint128 acc {0};
            for (int i {p}; i < numElements; i += numPartials)
            {
                acc += uint128{lhs[i]} * uint128{rhs[i]};
            }

            partials[p] = acc;
        });
    }).wait();

    uint128 device_total {0};
    for (int i {0}; i < numPartials; ++i)
    {
        device_total += partials[i];
    }

    // The same sum on the host, walking the data in both directions
    uint128 host_ascending {0};
    for (int i {0}; i < numElements; ++i)
    {
        host_ascending += uint128{lhs[i]} * uint128{rhs[i]};
    }

    uint128 host_descending {0};
    for (int i {numElements - 1}; i >= 0; --i)
    {
        host_descending += uint128{lhs[i]} * uint128{rhs[i]};
    }

    // What the same reduction produces with a 64-bit accumulator
    std::uint64_t narrow {0};
    for (int i {0}; i < numElements; ++i)
    {
        narrow += lhs[i] * rhs[i];
    }

    std::cout << std::endl;
    std::cout << "uint128 accumulator, exact and " << boost::int128::bit_width(host_ascending) << " bits wide" << std::endl;
    std::cout << "  device, " << numPartials << " strided partials : " << device_total << std::endl;
    std::cout << "  host, ascending order         : " << host_ascending << std::endl;
    std::cout << "  host, descending order        : " << host_descending << std::endl;
    std::cout << "  all three agree, so the partitioning of the reduction does not matter" << std::endl;

    std::cout << std::endl;
    std::cout << "uint64_t accumulator, silently wrong" << std::endl;
    std::cout << "  result                        : " << narrow << std::endl;
    std::cout << "  times it wrapped past 2^64    : " << (host_ascending >> 64U) << std::endl;

    // Widening to double is not a fix either: a single product already needs
    // more significand bits than double has.
    const uint128 exact_product {uint128{lhs[0]} * uint128{rhs[0]}};
    const double rounded_product {static_cast<double>(lhs[0]) * static_cast<double>(rhs[0])};
    const uint128 rounded_as_integer {rounded_product};
    const uint128 rounding_error {rounded_as_integer > exact_product ?
                                    rounded_as_integer - exact_product :
                                    exact_product - rounded_as_integer};

    std::cout << std::endl;
    std::cout << "double, too few significand bits for even one product" << std::endl;
    std::cout << "  lhs[0] * rhs[0] as uint128  : " << exact_product
              << " (" << boost::int128::bit_width(exact_product) << " bits)" << std::endl;
    std::cout << "  lhs[0] * rhs[0] as double     : " << std::setprecision(17) << rounded_product << std::endl;
    std::cout << "  rounding error                : " << rounding_error << std::endl;

    const bool exact {device_total == host_ascending && device_total == host_descending};

    std::cout << std::endl;
    if (exact)
    {
        std::cout << "All CPU and GPU computed elements match!" << std::endl;
    }
    else
    {
        std::cerr << "Result verification failed!" << std::endl;
    }

    sycl::free(lhs, q);
    sycl::free(rhs, q);
    sycl::free(partials, q);

    return exact ? EXIT_SUCCESS : EXIT_FAILURE;
}
Expected Output
[Exact dot product of 1048576 pairs of 40-bit integers]
SYCL device: Intel(R) ...

uint128 accumulator, exact and 99 bits wide
  device, 1024 strided partials : 317117374481213326859021202374
  host, ascending order         : 317117374481213326859021202374
  host, descending order        : 317117374481213326859021202374
  all three agree, so the partitioning of the reduction does not matter

uint64_t accumulator, silently wrong
  result                        : 5008986077640668102
  times it wrapped past 2^64    : 17190967317

double, too few significand bits for even one product
  lhs[0] * rhs[0] as uint128  : 583389027108597783863934 (79 bits)
  lhs[0] * rhs[0] as double     : 5.8338902710859781e+23
  rounding error                : 23584130

All CPU and GPU computed elements match!