Utilities
The <boost/int128/utilities.hpp> header collects helpers that operate on the library types directly and would not fit naturally into the analogous STL-style headers.
The functions are tuned specifically for uint128_t and int128_t rather than being template generalizations, which allows the library to dispatch to a fast path based on the shape of the modulus.
#include <boost/int128/utilities.hpp>
Modular Exponentiation
Computes (base ^ exp) mod m.
The naive expression pow(base, exp) % m is unusable for 128-bit inputs because base ^ exp overflows almost immediately; powm performs the reduction inside the exponentiation loop and selects an algorithm based on the modulus:
-
If
has_single_bit(m)istrue, modular reduction collapses to a bitmask and no division is performed. -
If the modulus fits in 64 bits (
m.high == 0), the loop runs on 64-bit lanes. Each squaring is a single 64x64 → 128 multiply followed by a 128-by-64 reduction. -
Otherwise the modulus uses the full 128 bits, and
powmuses a shift-and-add inner multiply so that no intermediate value ever exceeds 128 bits. This avoids forming the 256-bit product that a naive square-and-multiply implementation would require.
namespace boost {
namespace int128 {
BOOST_INT128_HOST_DEVICE constexpr uint128_t powm(uint128_t base, uint128_t exp, uint128_t m) noexcept;
BOOST_INT128_HOST_DEVICE constexpr int128_t powm(int128_t base, int128_t exp, int128_t m) noexcept;
} // namespace int128
} // namespace boost
The signed overload returns the non-negative residue in the range [0, m), matching the convention used by pow(a, b, m) in Python and most arbitrary-precision libraries.
Negative bases are reduced before exponentiation; (std::numeric_limits<int128_t>::min)() is handled correctly even though its magnitude is not representable in int128_t.
Special Cases
| Input | Result |
|---|---|
|
|
|
|
|
|
|
|
Signed overload with non-positive |
|
Integer Power
Computes base ^ exp by exponentiation by squaring, with a non-negative 64-bit exponent.
Unlike powm there is no modulus: the result is the true power reduced modulo 2^128, which is the same rollover behavior as the library’s operator*.
ipow(base, exp) is therefore equivalent to multiplying base by itself exp times.
namespace boost {
namespace int128 {
BOOST_INT128_HOST_DEVICE constexpr uint128_t ipow(uint128_t base, std::uint64_t exp) noexcept;
BOOST_INT128_HOST_DEVICE constexpr int128_t ipow(int128_t base, std::uint64_t exp) noexcept;
} // namespace int128
} // namespace boost
The exponent is unsigned, so negative powers (which are not integers) cannot be requested.
Because the result wraps on overflow rather than saturating or reporting an error, ipow is appropriate when rollover semantics are intended.
Integer Square Root
Computes the integer square root floor(sqrt(n)): the largest integer r whose square does not exceed n.
The computation runs entirely in integer arithmetic using Newton’s method, so it is exact (no floating-point rounding) and usable in a constexpr context.
namespace boost {
namespace int128 {
BOOST_INT128_HOST_DEVICE constexpr uint128_t isqrt(uint128_t n) noexcept;
BOOST_INT128_HOST_DEVICE constexpr int128_t isqrt(int128_t n) noexcept;
} // namespace int128
} // namespace boost
Checked Arithmetic
ckd_add, ckd_sub, and ckd_mul implement the checked integer arithmetic interface introduced by C23’s <stdckdint.h>, but without requiring a C23 toolchain; they are available in C++14 and later.
Each function computes a + b, a - b, or a * b respectively, as if both operands were represented in a signed integer type with infinite range, and then converts that mathematical result to the type pointed to by result.
The function returns false when *result correctly represents the mathematical result of the operation.
Otherwise it returns true, and *result is set to the mathematical result wrapped around (reduced modulo 2^N) to the width N of *result.
*result is always written, whether or not the operation overflowed.
namespace boost {
namespace int128 {
template <typename T1, typename T2, typename T3>
BOOST_INT128_HOST_DEVICE constexpr bool ckd_add(T1* result, T2 a, T3 b) noexcept;
template <typename T1, typename T2, typename T3>
BOOST_INT128_HOST_DEVICE constexpr bool ckd_sub(T1* result, T2 a, T3 b) noexcept;
template <typename T1, typename T2, typename T3>
BOOST_INT128_HOST_DEVICE constexpr bool ckd_mul(T1* result, T2 a, T3 b) noexcept;
} // namespace int128
} // namespace boost
The three type parameters are independent: the result type and the two operand types may differ in width and signedness. The operation always uses the exact mathematical value of each operand, so a negative signed value added to an unsigned value, or a product that needs up to 256 bits internally, is evaluated correctly.
Following the C23 rules, T1, T2, and T3 may be any integer type other than bool, plain char, an enumerated type, or a bit-precise (_BitInt) type.
In addition to the standard and extended integer types, the library’s uint128_t and int128_t are accepted.
The following example exercises all three operations, including the wrap-around, the INT128_MIN * -1 case, and the mixed-type behavior described above.
// 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
Integer Comparison
cmp_equal, cmp_not_equal, cmp_less, cmp_greater, cmp_less_equal, and cmp_greater_equal are the C26 https://eel.is/cdraft/utility.intcmp[integer comparison functions] extended to the library and builtin 128-bit types.
They are available in C++14 and later.
Unlike the built-in relational operators, which follow the usual arithmetic conversions, these functions compare the true mathematical values of their operands regardless of signedness.
When a uint128_t is compared with an int128_t, the operators convert the signed operand to unsigned, so int128_t{-1} < uint128_t{0} is reported as false and (std::numeric_limits<uint128_t>::max)() == int128_t{-1} as true.
The cmp_* functions report the mathematically correct answer in both cases.
Throughout this section a 128-bit type is int128_t, uint128_t, or a builtin 128-bit type where the platform provides one (__int128 and its unsigned counterpart, or std::_Signed128 and std::_Unsigned128 on MSVC).
namespace boost {
namespace int128 {
// Each function participates in overload resolution only when both operands are
// permitted and at least one is a 128-bit type. The other operand may be a
// 128-bit type or any standard or extended integer type other than bool, a
// character type, or std::byte.
template <typename T, typename U>
BOOST_INT128_HOST_DEVICE constexpr bool cmp_equal(T a, U b) noexcept;
template <typename T, typename U>
BOOST_INT128_HOST_DEVICE constexpr bool cmp_not_equal(T a, U b) noexcept;
template <typename T, typename U>
BOOST_INT128_HOST_DEVICE constexpr bool cmp_less(T a, U b) noexcept;
template <typename T, typename U>
BOOST_INT128_HOST_DEVICE constexpr bool cmp_greater(T a, U b) noexcept;
template <typename T, typename U>
BOOST_INT128_HOST_DEVICE constexpr bool cmp_less_equal(T a, U b) noexcept;
template <typename T, typename U>
BOOST_INT128_HOST_DEVICE constexpr bool cmp_greater_equal(T a, U b) noexcept;
} // namespace int128
} // namespace boost
cmp_equal(a, b) returns true when a and b are mathematically equal, and cmp_less(a, b) returns true when a is mathematically less than b.
The remaining four functions are defined in terms of these two exactly as in the standard: cmp_not_equal is !cmp_equal(a, b), cmp_greater(a, b) is cmp_less(b, a), cmp_less_equal(a, b) is !cmp_less(b, a), and cmp_greater_equal(a, b) is !cmp_less(a, b).
Every operand pair in which at least one operand is a 128-bit type is supported, including a uint128_t (or an unsigned builtin 128-bit value) against a signed int128_t (or a signed builtin one).
Following the standard, a bool, a character type (char, wchar_t, char8_t, char16_t, char32_t), or std::byte is not a permitted operand, and such a call is ill-formed.
Range Checking
in_range<R>(t) returns true when the value t is representable in the type R, that is, when R can hold t without the value changing.
It is equivalent to cmp_greater_equal(t, (std::numeric_limits<R>::min)()) && cmp_less_equal(t, (std::numeric_limits<R>::max)()), so the range check is itself signedness-safe.
namespace boost {
namespace int128 {
template <typename R, typename Integer>
BOOST_INT128_HOST_DEVICE constexpr bool in_range(Integer t) noexcept;
} // namespace int128
} // namespace boost
The target type R and the type of t may each be a builtin integer or a 128-bit type; at least one of them is a 128-bit type (use std::in_range when neither is).
For example, in_range<std::uint8_t>(int128_t{-1}) is false because a negative value is not representable in an unsigned type, and in_range<std::int64_t>std::numeric_limits<int128_t>::max)( is false because the value is too large.
// 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::cmp_equal;
using boost::int128::cmp_less;
using boost::int128::in_range;
std::cout << std::boolalpha;
constexpr auto u_max {(std::numeric_limits<uint128_t>::max)()};
// The built-in relational operators follow the usual arithmetic conversions,
// so comparing a uint128_t with a negative int128_t converts the negative
// value to a huge unsigned one and gives the wrong answer. The cmp_* family
// compares the true mathematical values instead.
std::cout << "=== Signedness-safe comparison ===" << std::endl;
std::cout << "UINT128_MAX == int128_t{-1} (operator): " << (u_max == int128_t{-1}) << std::endl;
std::cout << "cmp_equal(UINT128_MAX, int128_t{-1}): " << cmp_equal(u_max, int128_t{-1}) << std::endl;
std::cout << "int128_t{-1} < uint128_t{0} (operator): " << (int128_t{-1} < uint128_t{0}) << std::endl;
std::cout << "cmp_less(int128_t{-1}, uint128_t{0}): " << cmp_less(int128_t{-1}, uint128_t{0}) << std::endl;
// Either operand may be a builtin integer of any width and signedness.
std::cout << "\n=== Mixed with builtin integers ===" << std::endl;
std::cout << "cmp_less(int128_t{-5}, 0u): " << cmp_less(int128_t{-5}, 0U) << std::endl;
std::cout << "cmp_equal(uint128_t{42}, 42): " << cmp_equal(uint128_t{42}, 42) << std::endl;
// in_range<R>(v) reports whether v is representable in the target type R.
// R and the type of v may each be a builtin integer or a 128-bit type.
std::cout << "\n=== in_range ===" << std::endl;
std::cout << "in_range<std::int8_t>(int128_t{200}): " << in_range<std::int8_t>(int128_t{200}) << std::endl;
std::cout << "in_range<std::uint8_t>(int128_t{-1}): " << in_range<std::uint8_t>(int128_t{-1}) << std::endl;
std::cout << "in_range<std::uint64_t>(UINT128_MAX): " << in_range<std::uint64_t>(u_max) << std::endl;
std::cout << "in_range<int128_t>(UINT128_MAX): " << in_range<int128_t>(u_max) << std::endl;
return 0;
}
=== Signedness-safe comparison ===
UINT128_MAX == int128_t{-1} (operator): true
cmp_equal(UINT128_MAX, int128_t{-1}): false
int128_t{-1} < uint128_t{0} (operator): false
cmp_less(int128_t{-1}, uint128_t{0}): true
=== Mixed with builtin integers ===
cmp_less(int128_t{-5}, 0u): true
cmp_equal(uint128_t{42}, 42): true
=== in_range ===
in_range<std::int8_t>(int128_t{200}): false
in_range<std::uint8_t>(int128_t{-1}): false
in_range<std::uint64_t>(UINT128_MAX): false
in_range<int128_t>(UINT128_MAX): false