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) is true, 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 uint256 holds exactly, so each step is one multiply and one reduction.

  • Otherwise the modulus uses more than 64 bits, and powm uses 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

m == 0

0

m == 1

0

exp == 0

1 for m > 1 (following the conventional definition 0^0 == 1); the m == 0 and m == 1 rows above take precedence, so powm(0, 0, 0) and powm(0, 0, 1) return 0

base == 0 and exp > 0

0

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.

Special Cases

Input Result

exp == 0

1 (including ipow(0, 0) == 1, following the conventional definition 0^0 == 1)

base == 0 and exp > 0

0

base ^ exp exceeds 256 bits

The low 256 bits of the true power, matching the rollover of operator*

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.

Special Cases

Input Result

n < 2

n, so isqrt(0) == 0 and isqrt(1) == 1

otherwise

floor(sqrt(n)), the largest r whose square does not exceed n

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.

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

// Individual headers

#include <boost/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::max)() == -1 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(uint256{200}) is true while in_range(uint256{200}) 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.

Example 2. This example demonstrates signedness-safe comparison and range checking, ending with the corresponding 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