Utilities
The <boost/int256/utilities.hpp> header collects helpers that operate on the library type directly and would not fit naturally into the analogous STL-style headers.
The functions are tuned specifically for uint256 rather than being template generalizations, which allows the library to dispatch to a fast path based on the shape of the modulus.
#include <boost/int256/utilities.hpp>
Modular Exponentiation
Computes (base ^ exp) mod m.
The naive expression ipow(base, exp) % m is unusable for 256-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, the loop runs on 64-bit lanes. The product of two values below the modulus fits in 128 bits, which a
uint256holds exactly, so each step is one multiply and one reduction. -
Otherwise the modulus uses more than 64 bits, and
powmuses a shift-and-add inner multiply so that no intermediate value ever exceeds 256 bits. This avoids forming the 512-bit product that a naive square-and-multiply implementation would require.
namespace boost {
namespace int256 {
BOOST_INT256_HOST_DEVICE constexpr uint256 powm(uint256 base, uint256 exp, const uint256& m) noexcept;
BOOST_INT256_HOST_DEVICE constexpr int256 powm(const int256& base, const int256& exp, const int256& m) noexcept;
} // namespace int256
} // namespace boost
Special Cases
| Input | Result |
|---|---|
|
|
|
|
|
|
|
|
The int256 overload always returns a non-negative residue in [0, m), exactly as % would if m were positive and the intermediate power were computed with infinite range.
m ⇐ 0 or exp < 0 returns 0, since neither a non-positive modulus nor a negative exponent (which is not an integer power) is meaningful here.
A negative base is reduced into [0, m) before the (unsigned) exponentiation runs, rather than letting a negative residue come back out of it, so powm(int256{-1}, int256{1}, int256{7}) is 6, not -1.
Integer Power
Computes base ^ exp by exponentiation by squaring, with a 64-bit exponent.
Unlike powm there is no modulus: the result is the true power reduced modulo 2256, 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 int256 {
BOOST_INT256_HOST_DEVICE constexpr uint256 ipow(uint256 base, std::uint64_t exp) noexcept;
BOOST_INT256_HOST_DEVICE constexpr int256 ipow(int256 base, std::uint64_t exp) noexcept;
} // namespace int256
} // namespace boost
The exponent is unsigned, so negative powers (which are not integers) cannot be requested; base itself may be negative for the int256 overload, and the sign of the result then follows the ordinary rule for a negative number raised to an integer power.
Because the result wraps on overflow rather than saturating or reporting an error, ipow is appropriate when rollover semantics are intended; use saturating_mul in a loop when they are not.
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, which at this width would be wrong for most inputs) and usable in a constexpr context.
namespace boost {
namespace int256 {
BOOST_INT256_HOST_DEVICE constexpr uint256 isqrt(const uint256& n) noexcept;
BOOST_INT256_HOST_DEVICE constexpr int256 isqrt(const int256& n) noexcept;
} // namespace int256
} // namespace boost
For the int256 overload, a negative n returns 0: a negative value has no real square root, and 0 (rather than a precondition violation) keeps the function total.
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 int256 {
template <typename T1, typename T2, typename T3>
BOOST_INT256_HOST_DEVICE constexpr bool ckd_add(T1* result, T2 a, T3 b) noexcept;
template <typename T1, typename T2, typename T3>
BOOST_INT256_HOST_DEVICE constexpr bool ckd_sub(T1* result, T2 a, T3 b) noexcept;
template <typename T1, typename T2, typename T3>
BOOST_INT256_HOST_DEVICE constexpr bool ckd_mul(T1* result, T2 a, T3 b) noexcept;
} // namespace int256
} // 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 one is evaluated correctly, and a uint256 result from two 64-bit operands can never overflow.
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 uint256 and int256 are accepted, in any combination, so a result type of one sign can be computed from operands of the other.
The following example exercises all three operations, including the wrap-around and the mixed-type behavior described above, ending with int256 as both an operand and the result type.
// 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/int256/utilities.hpp>
#include <boost/int256/iostream.hpp>
#include <boost/int256/limits.hpp>
// Or you can do a single header
// #include <boost/int256.hpp>
#include <cstdint>
#include <iostream>
#include <limits>
int main()
{
using boost::int256::uint256;
using boost::int256::int256;
using boost::int256::ckd_add;
using boost::int256::ckd_sub;
using boost::int256::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 max_value {(std::numeric_limits<uint256>::max)()};
// A result that fits returns false and holds the exact value.
std::cout << "=== Results That Fit ===" << std::endl;
uint256 r {};
bool overflow {ckd_add(&r, uint256{20U}, uint256{22U})};
std::cout << "ckd_add(20, 22): overflow=" << overflow << ", result=" << r << std::endl;
// Addition that exceeds the type wraps modulo 2^256 and reports overflow.
std::cout << "\n=== Addition Overflow ===" << std::endl;
overflow = ckd_add(&r, max_value, uint256{1U});
std::cout << "ckd_add(max, 1): overflow=" << overflow << ", wrapped=" << r << 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(&r, uint256{0U}, uint256{1U});
std::cout << "ckd_sub(0, 1): overflow=" << overflow << ", wrapped=" << r << std::endl;
// Multiplication detects the overflow that operator* silently wraps. This is the
// case a 256-bit type runs into most often: the square of anything at or above
// 2^128 does not fit.
std::cout << "\n=== Multiplication Overflow ===" << std::endl;
constexpr uint256 two_to_128 {uint256{1U} << 128U};
overflow = ckd_mul(&r, two_to_128, two_to_128);
std::cout << "ckd_mul(2^128, 2^128): overflow=" << overflow << ", wrapped=" << r << std::endl;
overflow = ckd_mul(&r, two_to_128 - 1U, two_to_128 - 1U);
std::cout << "ckd_mul(2^128 - 1, 2^128 - 1): overflow=" << overflow << ", result=" << r
<< "\n (the largest product that still fits)" << 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::uint64_t narrow {};
overflow = ckd_add(&narrow, uint256{5U}, 3);
std::cout << "ckd_add<uint64_t>(uint256{5}, 3): overflow=" << overflow
<< ", result=" << narrow << std::endl;
overflow = ckd_add(&narrow, max_value, uint256{0U});
std::cout << "ckd_add<uint64_t>(max, 0): overflow=" << overflow
<< ", wrapped=" << narrow << std::endl;
// A wide result from narrow operands cannot overflow, which is a convenient way
// to get the exact product of two 128-bit values
overflow = ckd_mul(&r, UINT64_MAX, UINT64_MAX);
std::cout << "ckd_mul<uint256>(UINT64_MAX, UINT64_MAX): overflow=" << overflow
<< ", result=" << r << 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;
std::cout << "\n=== Checked Arithmetic with int256 ===" << std::endl;
constexpr auto int_max {(std::numeric_limits<int256>::max)()};
constexpr auto int_min {(std::numeric_limits<int256>::min)()};
int256 sr {};
overflow = ckd_add(&sr, int_max, int256{2});
std::cout << "ckd_add(INT256_MAX, 2): overflow=" << overflow << ", wrapped=" << sr << std::endl;
// Multiplication detects overflow that operator* would silently roll over,
// including MIN * -1, whose true result is not representable.
overflow = ckd_mul(&sr, int_max, int256{2});
std::cout << "ckd_mul(INT256_MAX, 2): overflow=" << overflow << ", wrapped=" << sr << std::endl;
overflow = ckd_mul(&sr, int_min, int256{-1});
std::cout << "ckd_mul(INT256_MIN, -1): overflow=" << overflow << ", wrapped=" << sr << std::endl;
// The result type and the two operand types are independent, and may
// differ in signedness; the exact mathematical value is always used.
std::int64_t narrow_signed {};
overflow = ckd_add(&narrow_signed, uint256{5U}, int256{-3});
std::cout << "ckd_add<int64_t>(uint256{5}, int256{-3}): overflow=" << overflow
<< ", result=" << narrow_signed << std::endl;
return 0;
}
Output:
=== Results That Fit ===
ckd_add(20, 22): overflow=false, result=42
=== Addition Overflow ===
ckd_add(max, 1): overflow=true, wrapped=0
=== Subtraction Underflow ===
ckd_sub(0, 1): overflow=true, wrapped=115792089237316195423570985008687907853269984665640564039457584007913129639935
=== Multiplication Overflow ===
ckd_mul(2^128, 2^128): overflow=true, wrapped=0
ckd_mul(2^128 - 1, 2^128 - 1): overflow=false, result=115792089237316195423570985008687907852589419931798687112530834793049593217025
(the largest product that still fits)
=== Mixed Types ===
ckd_add<uint64_t>(uint256{5}, 3): overflow=false, result=8
ckd_add<uint64_t>(max, 0): overflow=true, wrapped=18446744073709551615
ckd_mul<uint256>(UINT64_MAX, UINT64_MAX): overflow=false, result=340282366920938463426481119284349108225
ckd_mul<uint8_t>(20, 20): overflow=true, wrapped=144
=== Checked Arithmetic with int256 ===
ckd_add(INT256_MAX, 2): overflow=true, wrapped=-57896044618658097711785492504343953926634992332820282019728792003956564819967
ckd_mul(INT256_MAX, 2): overflow=true, wrapped=-2
ckd_mul(INT256_MIN, -1): overflow=true, wrapped=-57896044618658097711785492504343953926634992332820282019728792003956564819968
ckd_add<int64_t>(uint256{5}, int256{-3}): overflow=false, result=2
Integer Comparison
cmp_equal, cmp_not_equal, cmp_less, cmp_greater, cmp_less_equal, and cmp_greater_equal are the C++26 https://eel.is/cdraft/utility.intcmp[integer comparison functions] extended to `uint256`, `int256`, and the builtin 128-bit types.
They are available in pass:[C14] 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 uint256 is compared with a negative value the operators convert that value to unsigned, so uint256{0} > -1 is reported as false and (std::numeric_limits as true.
The cmp_* functions report the mathematically correct answer in both cases.
namespace boost {
namespace int256 {
// Each function participates in overload resolution only when both operands are
// permitted and at least one is uint256 or a builtin 128-bit type. The other
// operand may be any standard or extended integer type other than bool, a
// character type, or std::byte.
template <typename T, typename U>
BOOST_INT256_HOST_DEVICE constexpr bool cmp_equal(T a, U b) noexcept;
template <typename T, typename U>
BOOST_INT256_HOST_DEVICE constexpr bool cmp_not_equal(T a, U b) noexcept;
template <typename T, typename U>
BOOST_INT256_HOST_DEVICE constexpr bool cmp_less(T a, U b) noexcept;
template <typename T, typename U>
BOOST_INT256_HOST_DEVICE constexpr bool cmp_greater(T a, U b) noexcept;
template <typename T, typename U>
BOOST_INT256_HOST_DEVICE constexpr bool cmp_less_equal(T a, U b) noexcept;
template <typename T, typename U>
BOOST_INT256_HOST_DEVICE constexpr bool cmp_greater_equal(T a, U b) noexcept;
} // namespace int256
} // 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).
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 int256 {
template <typename R, typename Integer>
BOOST_INT256_HOST_DEVICE constexpr bool in_range(Integer t) noexcept;
} // namespace int256
} // namespace boost
The target type R and the type of t may each be a builtin integer, uint256, or int256; at least one of them is one of uint256, int256, or a builtin 128-bit type (use std::in_range when neither is).
For example, in_range is true while in_range is false, and in_range<uint256>(-1) is false because a negative value is not representable in an unsigned type.
in_range<int256>(t) and in_range<R>(int256{…}) follow the same mathematical-value rule: in_range<int256>(uint256_max) is false because uint256’s maximum exceeds `int256’s, and `in_range<uint256>(int256{-1}) is false because a negative source is never in an unsigned type’s range.
It is the natural guard in front of a narrowing conversion, which otherwise keeps only the low bits.
int256 cases.// 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/int256/utilities.hpp>
#include <boost/int256/iostream.hpp>
#include <boost/int256/limits.hpp>
// Or you can do a single header
// #include <boost/int256.hpp>
#include <cstdint>
#include <iostream>
#include <limits>
int main()
{
using boost::int256::uint256;
using boost::int256::int256;
using boost::int256::cmp_equal;
using boost::int256::cmp_less;
using boost::int256::in_range;
std::cout << std::boolalpha;
constexpr auto max_value {(std::numeric_limits<uint256>::max)()};
// The built-in relational operators follow the usual arithmetic conversions, so
// comparing a uint256 with a negative value converts the negative one to a huge
// unsigned value and gives the mathematically wrong answer. That is what the
// built-in unsigned types do too, and this library matches them deliberately.
// The cmp_* family compares the true mathematical values instead.
std::cout << "=== Signedness-safe comparison ===" << std::endl;
std::cout << "max == -1 (operator): " << (max_value == -1) << std::endl;
std::cout << "cmp_equal(max, -1): " << cmp_equal(max_value, -1) << std::endl;
std::cout << "uint256{0} > -1 (operator): " << (uint256{0U} > -1) << std::endl;
std::cout << "cmp_less(-1, uint256{0}): " << cmp_less(-1, uint256{0U}) << std::endl;
// Either operand may be a built-in integer of any width and signedness.
std::cout << "\n=== Mixed with builtin integers ===" << std::endl;
std::cout << "cmp_less(-5, uint256{0}): " << cmp_less(-5, uint256{0U}) << std::endl;
std::cout << "cmp_equal(uint256{42}, 42): " << cmp_equal(uint256{42U}, 42) << std::endl;
std::cout << "cmp_less(uint256{42}, 43U): " << cmp_less(uint256{42U}, 43U) << 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 built-in integer or uint256.
std::cout << "\n=== in_range ===" << std::endl;
std::cout << "in_range<std::uint8_t>(uint256{200}): " << in_range<std::uint8_t>(uint256{200U}) << std::endl;
std::cout << "in_range<std::int8_t>(uint256{200}): " << in_range<std::int8_t>(uint256{200U}) << std::endl;
std::cout << "in_range<std::uint64_t>(max): " << in_range<std::uint64_t>(max_value) << std::endl;
std::cout << "in_range<std::uint64_t>(UINT64_MAX): "
<< in_range<std::uint64_t>(uint256{UINT64_MAX}) << std::endl;
std::cout << "in_range<uint256>(-1): " << in_range<uint256>(-1) << std::endl;
std::cout << "in_range<uint256>(UINT64_MAX): " << in_range<uint256>(UINT64_MAX) << std::endl;
// A truncating conversion is easy to guard with in_range first
std::cout << "\n=== Guarding a narrowing conversion ===" << std::endl;
const uint256 candidates[] {uint256{7U}, uint256{UINT64_MAX}, max_value};
for (const auto& candidate : candidates)
{
if (in_range<std::uint64_t>(candidate))
{
std::cout << candidate << " fits in a std::uint64_t" << std::endl;
}
else
{
std::cout << candidate << " does not fit in a std::uint64_t" << std::endl;
}
}
std::cout << "\n=== int256 against a builtin integer is always exact ===" << std::endl;
// Every built-in integer, including unsigned __int128, fits inside the
// range of int256, so there is no mixed sign trap the way there is
// against uint256 below: the operator and cmp_less always agree.
constexpr int256 signed_min {(std::numeric_limits<int256>::min)()};
std::cout << "signed_min < -1 (operator): " << (signed_min < -1) << std::endl;
std::cout << "cmp_less(signed_min, -1): " << cmp_less(signed_min, -1) << std::endl;
std::cout << "\n=== int256 against uint256 has the same trap as unsigned ===" << std::endl;
// int256 and uint256 follow the usual arithmetic conversions: both become
// uint256, so a negative int256 becomes huge before the comparison, just
// like a negative builtin does against a uint256 above.
std::cout << "int256{-1} < uint256{1} (operator): " << (int256{-1} < uint256{1U}) << std::endl;
std::cout << "cmp_less(int256{-1}, uint256{1}): " << cmp_less(int256{-1}, uint256{1U}) << std::endl;
std::cout << "\n=== in_range with int256 ===" << std::endl;
// in_range<int256> from a uint256 tests against int256's positive range
std::cout << "in_range<int256>(uint256 max): " << in_range<int256>(max_value) << std::endl;
std::cout << "in_range<int256>(uint256{200}): " << in_range<int256>(uint256{200U}) << std::endl;
// in_range<uint256> from a negative int256 is always false
std::cout << "in_range<uint256>(int256{-1}): " << in_range<uint256>(int256{-1}) << std::endl;
std::cout << "in_range<std::int8_t>(int256{200}): " << in_range<std::int8_t>(int256{200}) << std::endl;
std::cout << "in_range<std::uint8_t>(int256{-1}): " << in_range<std::uint8_t>(int256{-1}) << std::endl;
return 0;
}
Output:
=== Signedness-safe comparison ===
max == -1 (operator): true
cmp_equal(max, -1): false
uint256{0} > -1 (operator): false
cmp_less(-1, uint256{0}): true
=== Mixed with builtin integers ===
cmp_less(-5, uint256{0}): true
cmp_equal(uint256{42}, 42): true
cmp_less(uint256{42}, 43U): true
=== in_range ===
in_range<std::uint8_t>(uint256{200}): true
in_range<std::int8_t>(uint256{200}): false
in_range<std::uint64_t>(max): false
in_range<std::uint64_t>(UINT64_MAX): true
in_range<uint256>(-1): false
in_range<uint256>(UINT64_MAX): true
=== Guarding a narrowing conversion ===
7 fits in a std::uint64_t
18446744073709551615 fits in a std::uint64_t
115792089237316195423570985008687907853269984665640564039457584007913129639935 does not fit in a std::uint64_t
=== int256 against a builtin integer is always exact ===
signed_min < -1 (operator): true
cmp_less(signed_min, -1): true
=== int256 against uint256 has the same trap as unsigned ===
int256{-1} < uint256{1} (operator): false
cmp_less(int256{-1}, uint256{1}): true
=== in_range with int256 ===
in_range<int256>(uint256 max): false
in_range<int256>(uint256{200}): true
in_range<uint256>(int256{-1}): false
in_range<std::int8_t>(int256{200}): false
in_range<std::uint8_t>(int256{-1}): false