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.
// 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_t;
using boost::int128::int128_t;
std::cout << "=== uint128_t Construction ===" << std::endl;
// 1) From a builtin integer type
constexpr uint128_t from_builtin {42U};
std::cout << "From builtin (42U): " << from_builtin << std::endl;
// 2) From high and low 64-bit values (high, low)
constexpr uint128_t from_parts {UINT64_C(0x1), UINT64_C(0x0)}; // 2^64
std::cout << "From parts (1, 0) = 2^64: " << from_parts << std::endl;
constexpr uint128_t 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_t>::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_t from_stream;
ss >> from_stream;
std::cout << "From stringstream: " << from_stream << std::endl;
std::cout << "\n=== int128_t Construction ===" << std::endl;
// Signed from builtin
constexpr int128_t 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_t 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_t>::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_t default_constructed {};
std::cout << "Default constructed: " << default_constructed << std::endl;
// Copy construction
const uint128_t 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_t from_double {12345.9};
std::cout << "uint128_t from 12345.9 (truncated): " << from_double << std::endl;
constexpr int128_t from_negative_double {-12345.9};
std::cout << "int128_t 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_t large_from_double {two_to_the_100};
std::cout << "uint128_t 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_t unsigned_from_nan {nan_value};
const int128_t signed_from_nan {nan_value};
std::cout << "uint128_t from NaN: " << unsigned_from_nan << std::endl;
std::cout << "int128_t from NaN: " << signed_from_nan << std::endl;
// Negative values are clamped to zero when constructing uint128_t.
const uint128_t unsigned_from_negative {-1.0};
std::cout << "uint128_t 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_t saturated_unsigned {infinity};
std::cout << "uint128_t from +infinity (saturates to UINT128_MAX): " << saturated_unsigned << std::endl;
// For int128_t, 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_t saturated_positive {huge};
const int128_t saturated_negative {-huge};
std::cout << "int128_t from 1e40 (saturates to INT128_MAX): " << saturated_positive << std::endl;
std::cout << "int128_t from -1e40 (saturates to INT128_MIN): " << saturated_negative << std::endl;
return 0;
}
=== uint128_t 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_t 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_t from 12345.9 (truncated): 12345 int128_t from -12345.9 (truncated toward zero): -12345 uint128_t from 2^100: 1267650600228229401496703205376 === Floating-Point Edge Cases === uint128_t from NaN: 0 int128_t from NaN: 0 uint128_t from -1.0 (clamped to zero): 0 uint128_t from +infinity (saturates to UINT128_MAX): 340282366920938463463374607431768211455 int128_t from 1e40 (saturates to INT128_MAX): 170141183460469231731687303715884105727 int128_t 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.
// 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_t;
using boost::int128::uint128_t;
// Basic arithmetic with signed 128-bit integers
int128_t x {1000000000000LL}; // 1 trillion
int128_t 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_t 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_t 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_t 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_t 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;
}
=== 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.
// 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_t;
using boost::int128::uint128_t;
std::cout << "=== Basic Streaming ===" << std::endl;
// Both types allow streaming as one would expect from a regular builtin-type
constexpr int128_t signed_value {-42};
std::cout << "Signed value: " << signed_value << std::endl;
// We can also use <iomanip> to change the output format
constexpr uint128_t 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_t two_to_64 {1, 0};
std::cout << "2^64 = " << two_to_64 << std::endl;
// 2^100 = a very large number
constexpr uint128_t two_to_100 {uint128_t{1} << 100};
std::cout << "2^100 = " << two_to_100 << std::endl;
// Maximum uint128_t value
constexpr auto uint_max {std::numeric_limits<uint128_t>::max()};
std::cout << "uint128_t max = " << uint_max << std::endl;
// Minimum and maximum int128_t values
constexpr auto int_min {std::numeric_limits<int128_t>::min()};
constexpr auto int_max {std::numeric_limits<int128_t>::max()};
std::cout << "int128_t min = " << int_min << std::endl;
std::cout << "int128_t max = " << int_max << std::endl;
// String conversion using stringstream
std::cout << "\n=== String Conversion with std::stringstream ===" << std::endl;
// Convert uint128_t to string
std::ostringstream oss;
oss << two_to_100;
auto str {oss.str()};
std::cout << "uint128_t to string: \"" << str << "\"" << std::endl;
// Convert string to uint128_t
std::istringstream iss {"123456789012345678901234567890"};
uint128_t parsed_value {};
iss >> parsed_value;
std::cout << "String to uint128_t: " << parsed_value << std::endl;
// Round-trip: value -> string -> value
std::cout << "\n=== Round-trip Conversion ===" << std::endl;
constexpr uint128_t original {0xDEADBEEF, 0xCAFEBABE12345678};
std::ostringstream oss2;
oss2 << original;
auto original_str {oss2.str()};
std::istringstream iss2 {original_str};
uint128_t 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;
}
=== 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_t max = 340282366920938463463374607431768211455 int128_t min = -170141183460469231731687303715884105728 int128_t max = 170141183460469231731687303715884105727 === String Conversion with std::stringstream === uint128_t to string: "1267650600228229401496703205376" String to uint128_t: 123456789012345678901234567890 === Round-trip Conversion === Original: 68915718020162848918556923512 As string: "68915718020162848918556923512" Round-tripped: 68915718020162848918556923512 Match: true
Rollover Behavior
Overflow is defined for both types.
uint128_t wraps modulo 2^128 as any unsigned integer does, and int128_t 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.
// 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_t;
using boost::int128::int128_t;
constexpr uint128_t max_unsigned_value {std::numeric_limits<uint128_t>::max()};
constexpr uint128_t min_unsigned_value {std::numeric_limits<uint128_t>::min()};
std::cout << "=== uint128_t behavior ===" << std::endl;
std::cout << "Max of uint128_t: " << max_unsigned_value << '\n'
<< "Max + 1U: " << max_unsigned_value + 1U << "\n\n";
std::cout << "Min of uint128_t: " << min_unsigned_value << '\n'
<< "Min - 1U: " << min_unsigned_value - 1U << "\n\n";
constexpr int128_t max_signed_value {std::numeric_limits<int128_t>::max()};
constexpr int128_t min_signed_value {std::numeric_limits<int128_t>::min()};
std::cout << "=== int128_t behavior ===" << std::endl;
std::cout << "Max of int128_t: " << max_signed_value << '\n'
<< "Max + 1: " << max_signed_value + 1 << "\n";
std::cout << "\nMin of int128_t: " << min_signed_value << '\n'
<< "Min - 1: " << min_signed_value - 1 << '\n' << std::endl;
}
=== uint128_t behavior === Max of uint128_t: 340282366920938463463374607431768211455 Max + 1U: 0 Min of uint128_t: 0 Min - 1U: 340282366920938463463374607431768211455 === int128_t behavior === Max of int128_t: 170141183460469231731687303715884105727 Max + 1: -170141183460469231731687303715884105728 Min of int128_t: -170141183460469231731687303715884105728 Min - 1: 170141183460469231731687303715884105727
Bitwise Functions (<bit>)
The <bit> functions are provided for uint128_t, 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.
// 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_t
constexpr boost::int128::uint128_t 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_t original{
0x0123456789ABCDEFULL,
0xFEDCBA9876543210ULL
};
// Expected result after byteswap
constexpr boost::int128::uint128_t 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.
// 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_t;
using boost::int128::int128_t;
// std::numeric_limits is overloaded for both types
constexpr auto uint_max {std::numeric_limits<uint128_t>::max()};
static_assert(std::is_same<decltype(uint_max), const uint128_t>::value, "Types should match");
constexpr auto int_max {std::numeric_limits<int128_t>::max()};
constexpr auto int_min {std::numeric_limits<int128_t>::min()};
std::cout << "=== Saturating Arithmetic ===" << std::endl;
std::cout << "uint128_t max = " << uint_max << std::endl;
std::cout << "int128_t max = " << int_max << std::endl;
std::cout << "int128_t 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_t{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_t>(uint_max) = " << boost::int128::saturating_cast<int128_t>(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;
}
=== Saturating Arithmetic === uint128_t max = 340282366920938463463374607431768211455 int128_t max = 170141183460469231731687303715884105727 int128_t 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_t>(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.
// 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_t;
using boost::int128::int128_t;
std::cout << "=== Greatest Common Divisor (gcd) ===" << std::endl;
// Basic gcd
constexpr uint128_t a {48};
constexpr uint128_t b {18};
std::cout << "gcd(" << a << ", " << b << ") = " << boost::int128::gcd(a, b) << std::endl;
// gcd with larger values
constexpr uint128_t large_a {123456789012345678ULL};
constexpr uint128_t 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_t huge_a {uint128_t{1} << 100};
constexpr uint128_t huge_b {uint128_t{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_t neg_a {-48};
constexpr int128_t 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_t x {12};
constexpr uint128_t y {18};
std::cout << "lcm(" << x << ", " << y << ") = " << boost::int128::lcm(x, y) << std::endl;
// lcm with coprime numbers
constexpr uint128_t p {7};
constexpr uint128_t 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_t low {10};
constexpr uint128_t high {20};
std::cout << "midpoint(" << low << ", " << high << ") = "
<< boost::int128::midpoint(low, high) << std::endl;
// Midpoint with odd sum (rounds toward first argument)
constexpr uint128_t odd_low {10};
constexpr uint128_t 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_t>::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_t neg {-100};
constexpr int128_t pos {100};
std::cout << "midpoint(" << neg << ", " << pos << ") = "
<< boost::int128::midpoint(neg, pos) << std::endl;
constexpr int128_t neg2 {-100};
constexpr int128_t neg3 {-50};
std::cout << "midpoint(" << neg2 << ", " << neg3 << ") = "
<< boost::int128::midpoint(neg2, neg3) << std::endl;
return 0;
}
=== 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.
// 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_t;
using boost::int128::int128_t;
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_t x {-12};
constexpr int128_t 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_t tie_x {-7};
constexpr int128_t 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_t modulus {7};
for (int128_t 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_t>::max)()};
constexpr uint128_t 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_t scale {1000};
const int128_t samples[] {int128_t{1500}, int128_t{2500}, int128_t{3500}, int128_t{-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;
}
=== 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.
// 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_t;
using boost::int128::int128_t;
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_t>::max()};
constexpr auto i_max {std::numeric_limits<int128_t>::max()};
constexpr auto i_min {std::numeric_limits<int128_t>::min()};
// A result that fits returns false and holds the exact value.
std::cout << "=== Results That Fit ===" << std::endl;
int128_t r {};
bool overflow {ckd_add(&r, int128_t{20}, int128_t{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_t u {};
overflow = ckd_add(&u, u_max, uint128_t{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_t{0}, uint128_t{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_t{2});
std::cout << "ckd_mul(INT128_MAX, 2): overflow=" << overflow << ", wrapped=" << r << std::endl;
overflow = ckd_mul(&r, i_min, int128_t{-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_t{5}, int128_t{-3});
std::cout << "ckd_add<int64_t>(uint128_t{5}, int128_t{-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;
}
=== 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_t{5}, int128_t{-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.
// 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_t, uint128_t, 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_t ===" << std::endl;
constexpr boost::int128::uint128_t 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_t ===" << std::endl;
constexpr boost::int128::int128_t 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;
}
=== Mixed Type Arithmetic with uint128_t === 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_t === 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.
// 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_t ===" << '\n';
// Setup our rng and distribution
std::mt19937_64 rng {42};
boost::random::uniform_int_distribution<boost::int128::uint128_t> dist {0, (std::numeric_limits<boost::int128::uint128_t>::max)()};
// Create a dataset for ourselves of random uint128_ts using our dist and rng from above
std::array<boost::int128::uint128_t, 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_t ===" << '\n';
// We can also generate random signed integers using int128_t
boost::random::uniform_int_distribution<boost::int128::int128_t> signed_dist {std::numeric_limits<boost::int128::int128_t>::min(), std::numeric_limits<boost::int128::int128_t>::max()};
std::cout << "Random int128_t: " << signed_dist(rng) << std::endl;
return 0;
}
=== uint128_t ===
Mean: 22125900135088040520646253247977468
Variance: 15183108029620265677746188314852225
Median: 169775281866460752209725324063124732284
=== int128_t ===
Random int128_t: 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.
// 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_t;
using boost::int128::int128_t;
char buffer[64];
// === to_chars: Convert integers to character strings ===
std::cout << "=== to_chars ===" << std::endl;
// Unsigned 128-bit to decimal string
constexpr uint128_t max_u128 {std::numeric_limits<uint128_t>::max()};
auto result {boost::charconv::to_chars(buffer, buffer + sizeof(buffer), max_u128)};
*result.ptr = '\0';
std::cout << "uint128_t max (decimal): " << buffer << std::endl;
// Signed 128-bit to decimal string
constexpr int128_t min_i128 {std::numeric_limits<int128_t>::min()};
result = boost::charconv::to_chars(buffer, buffer + sizeof(buffer), min_i128);
*result.ptr = '\0';
std::cout << "int128_t min (decimal): " << buffer << std::endl;
// Hexadecimal output (base 16)
uint128_t 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_t (hex): 0x" << buffer << std::endl;
// Octal output (base 8)
result = boost::charconv::to_chars(buffer, buffer + sizeof(buffer), int128_t{511}, 8);
*result.ptr = '\0';
std::cout << "int128_t 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_t
const char* decimal_str {"340282366920938463463374607431768211455"};
uint128_t 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_t
const char* negative_str {"-170141183460469231731687303715884105728"};
int128_t 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_t 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;
}
=== to_chars === uint128_t max (decimal): 340282366920938463463374607431768211455 int128_t min (decimal): -170141183460469231731687303715884105728 uint128_t (hex): 0xdeadbeefcafebabe12345678 int128_t 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.
// 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_t;
using boost::int128::int128_t;
// 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_t x;
int128_t 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_t big {UINT64_C(0xDEADBEEF), UINT64_C(0xCAFEBABE12345678)};
const int128_t neg {-123456789012345678LL};
std::cout << "boost::hash matches std::hash (uint128_t): "
<< std::boolalpha << (boost::hash<uint128_t>{}(big) == std::hash<uint128_t>{}(big)) << std::endl;
std::cout << "boost::hash matches std::hash (int128_t): "
<< (boost::hash<int128_t>{}(neg) == std::hash<int128_t>{}(neg)) << std::endl;
std::cout << "\n=== boost::unordered_map<uint128_t, ...> ===" << std::endl;
// boost::unordered_map defaults to boost::hash<Key>, so uint128_t keys need
// no explicit hasher.
boost::unordered_map<uint128_t, std::string> labels {};
labels[uint128_t{1, 0}] = "two to the sixty-fourth"; // 2^64
labels[uint128_t{UINT64_C(0x8000000000000000), 0}] = "two to the one hundred twenty-seventh"; // 2^127
labels[uint128_t{42}] = "forty-two";
std::cout << "Entries: " << labels.size() << std::endl;
std::cout << "Label at 2^64: " << labels[uint128_t{1, 0}] << std::endl;
std::cout << "Contains 42: " << (labels.find(uint128_t{42}) != labels.end()) << std::endl;
std::cout << "\n=== hash_combine for a composite key ===" << std::endl;
// The point hasher combines two int128_t 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_t, ...> ===" << std::endl;
// The modern flat container also defaults to boost::hash.
boost::unordered_flat_map<int128_t, int> counts {};
counts[int128_t{-1}] = 1;
counts[int128_t{0}] = 2;
counts[int128_t{1}] = 3;
std::cout << "Flat map size: " << counts.size() << std::endl;
std::cout << "counts[-1] = " << counts[int128_t{-1}] << std::endl;
return 0;
}
=== boost::hash on 128-bit types === boost::hash matches std::hash (uint128_t): true boost::hash matches std::hash (int128_t): true === boost::unordered_map<uint128_t, ...> === 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_t, ...> === 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_t is larger than the maximum int128_t, so it cannot be written as a negated literal.
The NOTE following the output explains what to use instead.
// 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_t;
using boost::int128::int128_t;
using boost::int128::to_string;
using namespace boost::int128::literals;
std::cout << "=== to_string with uint128_t ===" << std::endl;
// Compare against std::to_string for values that fit in 64 bits
constexpr uint128_t 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_t 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_t 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_t 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_t max: " << to_string(large_unsigned) << std::endl;
std::cout << "\n=== to_string with int128_t ===" << std::endl;
// Compare against std::to_string for values that fit in 64 bits
constexpr int128_t 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_t 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_t 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_t 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_t min with string literal: " << to_string(large_negative) << std::endl;
const auto large_negative_c {BOOST_INT128_INT128_C(-170141183460469231731687303715884105728)};
std::cout << "\nint128_t min with INT128_C macro: " << to_string(large_negative_c) << std::endl;
const auto large_positive {std::numeric_limits<int128_t>::max()};
std::cout << "int128_t max: " << to_string(large_positive) << std::endl;
return 0;
}
=== to_string with uint128_t === uint128_t to_string(1234567890): 1234567890 std::to_string(uint64_t 1234567890): 1234567890 Match: true uint128_t to_string(UINT64_MAX): 18446744073709551615 std::to_string(UINT64_MAX): 18446744073709551615 Match: true uint128_t max: 340282366920938463463374607431768211455 === to_string with int128_t === int128_t to_string(-42): -42 std::to_string(int64_t -42): -42 Match: true int128_t to_string(INT64_MAX): 9223372036854775807 std::to_string(INT64_MAX): 9223372036854775807 Match: true int128_t min with string literal: 0 int128_t min with INT128_C macro: -170141183460469231731687303715884105728 int128_t max: 170141183460469231731687303715884105727
The line int128_t min with string literal: 0 is expected. The positive magnitude of the minimum int128_t value (2^127) is larger than the maximum int128_t value, so it cannot be written as a negated _i128 literal. Construct it with BOOST_INT128_INT128_C or (std::numeric_limits<int128_t>::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::.
// 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_t;
using boost::int128::uint128_t;
std::cout << "=== Basic Formatting ===" << std::endl;
constexpr uint128_t unsigned_value {0xDEADBEEF, 0xCAFEBABE12345678};
constexpr int128_t 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_t 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_t positive {42};
constexpr int128_t 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_t>::max()};
constexpr auto int_min {std::numeric_limits<int128_t>::min()};
std::cout << fmt::format("uint128_t max: {}", uint_max) << std::endl;
std::cout << fmt::format("uint128_t max (hex): {:#x}", uint_max) << std::endl;
std::cout << fmt::format("int128_t 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;
}
=== 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_t max: 340282366920938463463374607431768211455 uint128_t max (hex): 0xffffffffffffffffffffffffffffffff int128_t 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.
// 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_t;
using boost::int128::int128_t;
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_t dividend {1000000000000000000ULL};
constexpr uint128_t 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_t large_dividend {uint128_t{1} << 100}; // 2^100
constexpr uint128_t large_divisor {uint128_t{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_t signed_dividend {-100};
constexpr int128_t 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_t pos {17};
constexpr int128_t neg {-17};
constexpr int128_t div_pos {5};
constexpr int128_t 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_t{3}, uint128_t{10})};
std::cout << "3 / 10 = " << small_div.quot << " remainder " << small_div.rem << std::endl;
return 0;
}
=== 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.
// 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_t;
// 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;
}
[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.
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_t;
// 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;
}
[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_t supplies that
intermediate in device code, where unsigned __int128 is not portably available.
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_t 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_t;
// 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_t{a} * uint128_t{b}) % uint128_t{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_t, so a^d mod n is exact
auto x {static_cast<std::uint64_t>(boost::int128::powm(uint128_t{bases[i]}, uint128_t{d}, uint128_t{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_t{a} * uint128_t{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_t: " << 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;
}
Full 128-bit product a * b: 159945956516994065446882290339781513595 (a * b) % n, 64-bit math: 17693923505768731003 <- the product wrapped (a * b) % n via uint128_t: 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_t accumulator is both
exact and independent of how the reduction is split across the device.
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_t 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_t;
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_t* partials {sycl::malloc_shared<uint128_t>(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_t acc {0};
for (int i {p}; i < numElements; i += numPartials)
{
acc += uint128_t{lhs[i]} * uint128_t{rhs[i]};
}
partials[p] = acc;
});
}).wait();
uint128_t 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_t host_ascending {0};
for (int i {0}; i < numElements; ++i)
{
host_ascending += uint128_t{lhs[i]} * uint128_t{rhs[i]};
}
uint128_t host_descending {0};
for (int i {numElements - 1}; i >= 0; --i)
{
host_descending += uint128_t{lhs[i]} * uint128_t{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_t 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_t exact_product {uint128_t{lhs[0]} * uint128_t{rhs[0]}};
const double rounded_product {static_cast<double>(lhs[0]) * static_cast<double>(rhs[0])};
const uint128_t rounded_as_integer {rounded_product};
const uint128_t 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_t : " << 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;
}
[Exact dot product of 1048576 pairs of 40-bit integers] SYCL device: Intel(R) ... uint128_t 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_t : 583389027108597783863934 (79 bits) lhs[0] * rhs[0] as double : 5.8338902710859781e+23 rounding error : 23584130 All CPU and GPU computed elements match!