Mixed Type Operations

Conversions

The constructors from, and the conversion operators to, the built-in integer and floating-point types are all implicit, matching the built-in integer types. The single exception is operator bool, which is explicit. See Constructors and Conversions.

uint256 and int256 also convert to each other implicitly, and that conversion is a plain bit copy in both directions: no bits change, only the interpretation of bit 63 of words[3] does. See int256 Constructors.

int256 and uint256

int256 and uint256 interoperate the way a built-in signed and unsigned integer of the same width do: through the usual arithmetic conversions, with the signed operand converted to the unsigned type. Concretely, both operands convert to uint256 first, so a negative int256 sign-extends into all four words exactly as a negative built-in integer does when compared or combined with an unsigned one of the same width.

Operands Common type Result of arithmetic / bitwise

int256 and uint256

uint256

uint256

For example, int256{-1} < uint256{1} is false, for the same reason -1 < 1U is at 32 bits: the -1 becomes BOOST_INT256_UINT256_MAX before the comparison. Use the cmp_* family when the mathematical answer is wanted instead.

For shift operators, the result type follows the left operand, exactly as it does for every other right operand type: int256 << uint256 and int256 >> uint256 return int256; uint256 << int256 and uint256 >> int256 return uint256. Only the right operand’s integer value is used as the count; its type does not otherwise participate.

namespace boost {
namespace int256 {

//=====================================
// Comparison Operators
//=====================================

constexpr bool operator==(const uint256& lhs, const int256& rhs);
constexpr bool operator==(const int256& lhs, const uint256& rhs);
constexpr bool operator!=(const uint256& lhs, const int256& rhs);
constexpr bool operator!=(const int256& lhs, const uint256& rhs);
constexpr bool operator<(const uint256& lhs, const int256& rhs);
constexpr bool operator<(const int256& lhs, const uint256& rhs);
constexpr bool operator<=(const uint256& lhs, const int256& rhs);
constexpr bool operator<=(const int256& lhs, const uint256& rhs);
constexpr bool operator>(const uint256& lhs, const int256& rhs);
constexpr bool operator>(const int256& lhs, const uint256& rhs);
constexpr bool operator>=(const uint256& lhs, const int256& rhs);
constexpr bool operator>=(const int256& lhs, const uint256& rhs);

//=====================================
// Arithmetic Operators
//=====================================

constexpr uint256 operator+(const uint256& lhs, const int256& rhs);
constexpr uint256 operator+(const int256& lhs, const uint256& rhs);
constexpr uint256 operator-(const uint256& lhs, const int256& rhs);
constexpr uint256 operator-(const int256& lhs, const uint256& rhs);
constexpr uint256 operator*(const uint256& lhs, const int256& rhs);
constexpr uint256 operator*(const int256& lhs, const uint256& rhs);
constexpr uint256 operator/(const uint256& lhs, const int256& rhs);
constexpr uint256 operator/(const int256& lhs, const uint256& rhs);
constexpr uint256 operator%(const uint256& lhs, const int256& rhs);
constexpr uint256 operator%(const int256& lhs, const uint256& rhs);

//=====================================
// Bitwise Operators
//=====================================

constexpr uint256 operator|(const uint256& lhs, const int256& rhs);
constexpr uint256 operator|(const int256& lhs, const uint256& rhs);
constexpr uint256 operator&(const uint256& lhs, const int256& rhs);
constexpr uint256 operator&(const int256& lhs, const uint256& rhs);
constexpr uint256 operator^(const uint256& lhs, const int256& rhs);
constexpr uint256 operator^(const int256& lhs, const uint256& rhs);

//=====================================
// Shift Operators
//=====================================
// Result type follows the left operand.

constexpr int256  operator<<(const int256&  lhs, const uint256& rhs);
constexpr uint256 operator<<(const uint256& lhs, const int256&  rhs);
constexpr int256  operator>>(const int256&  lhs, const uint256& rhs);
constexpr uint256 operator>>(const uint256& lhs, const int256&  rhs);

} // namespace int256
} // namespace boost

The cross-type arithmetic and bitwise operators return the same value as static_cast<uint256>(lhs) op static_cast<uint256>(rhs). The comparison operators return the same value as that expression compared with the matching unsigned operator.

Compound Assignment Is the One Exception

Every other compound assignment operator in this library is this = *this op rhs, which is why Compound Assignment onto a Built-in Integer below can describe all of them at once. int256& operator@=(int256&, const uint256&) (for all ten of |=, &=, ^=, <<=, >>=, +=, -=, =, /=, %=) is declared separately rather than falling out of that rule, because this = *this op rhs would mean int256 = uint256, which converts back to int256 *before the division or shift happens; that changes the answer whenever *this is negative.

Instead, each of these ten operators is defined to convert *this to uint256 explicitly, perform the operation entirely in the uint256 domain, and convert only the final result back to int256, exactly matching what long long op= unsigned long long does at 64 bits:

boost::int256::int256 x {-10};
x /= boost::int256::uint256{3};   // (uint256)-10 / 3, then converted back to int256

This is a deliberate correction relative to Boost.Int128, where int128 /= uint128 has no cross-type compound overload and therefore resolves to the member int128::operator/=(int128), dividing signed where the built-in mixed-type operation (and this library’s own int256 /= uint256) divides unsigned. uint256 @= int256 needs no such special case: it already resolves through uint256::operator@=(const uint256&), because uint256 = int256 converting *this back is a no-op (the target is already unsigned).

(uint256 op= int256’s compound assignment, and all of `int256’s and `uint256’s compound assignment onto a built-in integer, follow the ordinary `*this = *this op rhs rule and are covered by Compound Assignment onto a Built-in Integer below.)

Operator Overloads Across Types

The rest of this page covers uint256 against a built-in type. int256 against a built-in type follows the same rules with int256 in place of uint256 throughout (see int256’s own operator behavior for the one difference: every builtin, including unsigned __int128, fits inside int256, so there is no mixed sign trap the way there is between int256 and uint256 above). int256 against uint256 itself was covered above in int256 and uint256.

All comparison, arithmetic, bitwise, and shift operators are provided between uint256 and any built-in integer type, signed or unsigned, including the compiler’s __int128 and unsigned __int128 where those exist (and std::_Signed128 / std::_Unsigned128 on MSVC in C++20 mode). The same operators are provided against Boost.Int128’s uint128 and int128 when <boost/int128.hpp> is included first; see Operations With Boost.Int128 Types below.

The compound assignment operators are provided in both operand orders, so a built-in integer accepts a uint256 right operand as well; see Compound Assignment onto a Built-in Integer below.

Additionally, the operators that the built-in integers allow with a floating-point operand are provided between uint256 and float, double, and long double; see Operations With Floating-Point Types below.

The behavior and return type of every mixed overload follow the C++ usual arithmetic conversions, identical to what the equivalent built-in unsigned operation would produce at 256 bits, including two’s-complement wrap-around semantics.

Result Type Rules

Operands Common type Result of arithmetic / bitwise

uint256 and a signed built-in (int8_t to int64_t, or __int128)

uint256

uint256

uint256 and an unsigned built-in (uint8_t to uint64_t, or unsigned __int128)

uint256

uint256

uint256 and float

float

float

uint256 and double

double

double

uint256 and long double

long double

long double

A signed operand is converted to uint256 first, which sign-extends a negative value into all four words, so uint256{3} - -1 is 4 and uint256{3} > -1 is false. That is what the built-in unsigned types do with a negative operand; use the cmp_* family when the mathematical answer is wanted.

For shift operators (<<, >>), the result type follows the left operand after integral promotion, regardless of the right operand, matching the built-in shift rules. uint256 is not promoted, so a uint256 left operand yields itself. A built-in left operand of lesser rank than int promotes, and does so whether or not it is signed, so bool, unsigned char, and unsigned short all yield a signed int. A shift count that is negative or greater than or equal to the width of the shifted operand, which is 256 when the left operand is a uint256, is undefined behavior, exactly as for the built-in shift operators; see the shift operator reference for the precise rules.

For comparison operators (==, !=, <, <=, >, >=), the return type is always bool and the comparison is performed on the operands after they have been converted to the common type above.

Operations with built-in __int128 / unsigned __int128

When the compiler provides 128-bit built-in integer types, every operator above is also available with one of them as an operand, in either order. The 128-bit operand is converted to uint256 first (sign-extended when it is the signed type), so the result type follows the same rules as the table above: uint256 for arithmetic and bitwise, the left operand for shifts, bool for comparisons.

These overloads are declared BOOST_INT256_BUILTIN_CONSTEXPR rather than plain constexpr, because the MSVC simulated 128-bit types are not usable in a constant expression before Visual Studio 14.45. They disappear entirely under BOOST_INT256_NO_BUILTIN_INT128.

The compiler’s 128-bit type is used as a wide intermediate inside the library, never as storage. A uint256 always holds four 64-bit words, so results do not depend on whether these overloads exist.

Compound Assignment onto a Built-in Integer

Every compound assignment operator is available with a built-in integer on the left and uint256 on the right:

using boost::int256::uint256;

unsigned flags {0};
flags |= uint256{1U};      // 1, and flags is still unsigned

int counter {12};
counter += uint256{5U};    // 17

i op= v is equivalent to i = static_cast<decltype(i)>(i op v) using the binary operator from the table above, which is what the built-in integers do. The whole operation is performed in the common type of the two operands and only the result is converted back, so a right operand wider than the left operand is never truncated first:

std::uint32_t word {0};
word += (uint256{1U} << 64U) + uint256{7U};   // 7, the low word of 2^64 + 7

std::uint64_t big {8};
big /= uint256{1U} << 64U;                    // 0, because 8 / 2^64 is zero

The left operand keeps its own type, so a result outside its range wraps exactly as the built-in wraps. The shift operators are the exception the language makes for all integer types: they take the value and the result type from the left operand alone, and only the count from the right.

std::uint64_t bits {1};
bits <<= uint256{40U};     // 2^40, a 64-bit shift by a count of 40

Available Types and Operators

Integer is any built-in integer type, signed or unsigned, from bool through long long, including the compiler’s __int128 and unsigned __int128 where supported.

namespace boost {
namespace int256 {

template <BOOST_INT256_INTEGER_CONCEPT Integer>
BOOST_INT256_HOST_DEVICE constexpr Integer& operator|=(Integer& lhs, const uint256& rhs);
// and likewise for &=, ^=, +=, -=, *=, /=, %=, <<=, >>=

} // namespace int256
} // namespace boost

A division or remainder by zero, and a shift count that is negative or at least the width of the promoted left operand, are undefined, exactly as for the built-in operators.

Operations With Boost.Int128 Types

Every operation this library provides against the builtin __int128 and unsigned __int128 is also provided against Boost.Int128's boost::int128::int128 and boost::int128::uint128, on every platform, including the ones with no builtin 128-bit type. Boost.Int256 never includes <boost/int128.hpp> itself, so this support drags nothing in and costs nothing unless it is asked for. It is enabled by including Boost.Int128 first:

#include <boost/int128.hpp>   // before any Boost.Int256 header
#include <boost/int256.hpp>

boost::int256::int256 x {boost::int128::int128{-42}};   // -42
x *= boost::int128::uint128{3U};                         // -126

boost::int128::uint128 low = x;                          // the low 128 bits of -126

If <boost/int128.hpp> is only included after a Boost.Int256 header, the two libraries stay unaware of each other, and mixing their types is a compile error rather than a silent conversion.

Behavior

uint128 and int128 behave exactly like the builtin 128-bit types they model:

  • Construction of uint256 or int256 from uint128 or int128 is implicit and exact. An int128 sign fills the upper words, so uint256{int128{-1}} is the maximum uint256.

  • The conversions from uint256 or int256 to uint128 or int128 are implicit and keep the low 128 bits.

  • A binary operator converts the 128-bit operand to the 256-bit type first, so the result has the type of the 256-bit operand and the usual arithmetic conversions apply: uint256{0} < int128{-1} is true because the -1 becomes the maximum uint256, while int256{0} > int128{-1} is true because every 128-bit value fits in int256.

  • A shift takes its value and its result type from the left operand, and only the count from the right, so uint128{1} << uint256{100} is a uint128.

  • Every compound assignment works in both operand orders. With the Boost.Int128 type on the left, the operation happens at 256 bits and only the result is narrowed, so dividing a uint128 by uint256{1} << 200 gives zero rather than dividing by a truncated divisor.

  • saturating_cast, ckd_add, ckd_sub, ckd_mul, the cmp_* family, and in_range accept uint128 and int128 as operands, and as the target type.

Operands Result of arithmetic / bitwise

uint256 and uint128 or int128

uint256

int256 and uint128 or int128

int256

Available Operators

In the synopsis below Int128Type is boost::int128::uint128 or boost::int128::int128, and Wide stands for each of uint256 and int256.

namespace boost {
namespace int256 {

// Comparisons, both operand orders
template <typename Int128Type>
BOOST_INT256_HOST_DEVICE constexpr bool operator==(const Wide& lhs, const Int128Type rhs) noexcept;

template <typename Int128Type>
BOOST_INT256_HOST_DEVICE constexpr bool operator==(const Int128Type lhs, const Wide& rhs) noexcept;

// and likewise for !=, <, <=, >, >=, and <=> (which returns std::strong_ordering)

// Arithmetic and bitwise operators, both operand orders
template <typename Int128Type>
BOOST_INT256_HOST_DEVICE constexpr Wide operator+(const Wide& lhs, const Int128Type rhs) noexcept;

template <typename Int128Type>
BOOST_INT256_HOST_DEVICE constexpr Wide operator+(const Int128Type lhs, const Wide& rhs) noexcept;

// and likewise for -, *, /, %, &, |, ^

// Shifts: the result type follows the left operand
template <typename Int128Type>
BOOST_INT256_HOST_DEVICE constexpr Wide operator<<(const Wide& lhs, const Int128Type rhs) noexcept;

template <typename Int128Type>
BOOST_INT256_HOST_DEVICE constexpr Int128Type operator<<(const Int128Type lhs, const Wide& rhs) noexcept;

// and likewise for >>

// Compound assignment, both operand orders
template <typename Int128Type>
BOOST_INT256_HOST_DEVICE constexpr Wide& operator+=(Wide& lhs, const Int128Type rhs) noexcept;

template <typename Int128Type>
BOOST_INT256_HOST_DEVICE constexpr Int128Type& operator+=(Int128Type& lhs, const Wide& rhs) noexcept;

// and likewise for -=, *=, /=, %=, &=, |=, ^=, <<=, >>=

} // namespace int256
} // namespace boost

A division or remainder by zero, and a shift count that is negative or at least the width of the left operand, are undefined, exactly as for the builtin 128-bit types.

Limitations

  • The include order is the switch, and it is decided once per translation unit by the first Boost.Int256 header included.

  • To convert a 256-bit value to a Boost.Int128 type, use copy initialization (boost::int128::uint128 v = x;), assignment, or pass it where a uint128 or int128 is expected; these work with every compiler and language standard. A static_cast or other direct initialization works with GCC and Clang from C++17 onward, but is ambiguous under C++14 with every compiler, and with MSVC in C++20 mode. The cause is on the Boost.Int128 side: its own constructors (from the builtin 128-bit types, and uint128 from int128) are also reachable through the other implicit conversions of uint256 and int256, and direct initialization only prefers the conversion that yields the target type exactly from C++17 onward, which MSVC does not apply once its builtin 128-bit types are in play.

  • Boost.Int128 has its own unconstrained ckd_add, ckd_sub, and ckd_mul, so an unqualified call with operands from both libraries is ambiguous through argument dependent lookup. Qualify the call: boost::int256::ckd_add(&result, x, v).

  • The C++20 module (import boost.int256;) does not provide this interoperability, because a module interface cannot see which headers the importing translation unit included.

Operations With Floating-Point Types

uint256 supports exactly the operators that the built-in integers support with a floating-point operand, in either operand order, for float, double, long double, and, where the standard library provides it, a C++23 <stdfloat> extended type (std::float16_t, float32_t, float64_t, float128_t, bfloat16_t).

The rule is the C++ usual arithmetic conversions: the 256-bit operand is converted to the floating-point type first, and the operation is then performed entirely in that floating-point type. The result type is the floating-point type, never uint256.

boost::int256::uint256 u {5U};

auto a = u + 1.0;      // double, 6.0
auto b = 1.0F - u;     // float, -4.0F
auto c = u * 0.5;      // double, 2.5   (the operand is NOT truncated to 0 first)
bool d = u < 5.5;      // true

Because the conversion happens before the operation, and because it is rounded once, to nearest with ties to even, every result is what the corresponding built-in expression would produce. That holds on every platform and in either builtin configuration: the conversion is implemented in the library, so it does not depend on the compiler having a wide integer type.

The <stdfloat> types are host only, and only available at all under C++23 or later on a toolchain that actually ships the specific type (GCC 13 and later with libstdc; as of this writing, libc and MSVC’s standard library do not yet provide it). Where a type is not available, it simply does not participate in overload resolution here, exactly like any other type the platform lacks: there is no error, just no operator. The conversion to and from these types is correctly rounded exactly as above, so mixing with std::float16_t (8-bit significand, no value above 65504) can overflow to infinity where double or long double would not.

Under -ffast-math, -ffinite-math-only, or the Intel compiler’s default -fp-model=fast, the compiler may assume no floating-point operand is ever NaN or infinity, so a conversion from one of those values (in either direction of these mixed-type operators) gives unspecified results. Use -fp-model=precise with icpx when NaN or infinity handling matters. This was found in CI, where icpx’s default model turned `uint256{+inf} into 2^191.

Available Operators

namespace boost {
namespace int256 {

// Float is float, double, long double, or a <stdfloat> extended type the
// standard library provides.

//=====================================
// Arithmetic Operators
//=====================================

template <BOOST_INT256_FLOATING_POINT_CONCEPT Float>
BOOST_INT256_HOST_DEVICE constexpr Float operator+(const uint256& lhs, Float rhs);
template <BOOST_INT256_FLOATING_POINT_CONCEPT Float>
BOOST_INT256_HOST_DEVICE constexpr Float operator+(Float lhs, const uint256& rhs);
// and likewise for -, *, /

//=====================================
// Comparison Operators
//=====================================

template <BOOST_INT256_FLOATING_POINT_CONCEPT Float>
BOOST_INT256_HOST_DEVICE constexpr bool operator==(const uint256& lhs, Float rhs);
template <BOOST_INT256_FLOATING_POINT_CONCEPT Float>
BOOST_INT256_HOST_DEVICE constexpr bool operator==(Float lhs, const uint256& rhs);
// and likewise for !=, <, <=, >, >=

// Requires C++20. A mixed integer and floating-point comparison is a partial
// ordering, because a NaN operand is unordered with respect to every value
template <BOOST_INT256_FLOATING_POINT_CONCEPT Float>
BOOST_INT256_HOST_DEVICE constexpr std::partial_ordering operator<=>(const uint256& lhs, Float rhs);
template <BOOST_INT256_FLOATING_POINT_CONCEPT Float>
BOOST_INT256_HOST_DEVICE constexpr std::partial_ordering operator<=>(Float lhs, const uint256& rhs);

//=====================================
// Compound Assignment
//=====================================

template <BOOST_INT256_FLOATING_POINT_CONCEPT Float>
BOOST_INT256_HOST_DEVICE constexpr uint256& uint256::operator+=(Float rhs);
// and likewise for -=, *=, /=

template <BOOST_INT256_FLOATING_POINT_CONCEPT Float>
BOOST_INT256_HOST_DEVICE constexpr Float& operator+=(Float& lhs, const uint256& rhs);
// and likewise for -=, *=, /=

} // namespace int256
} // namespace boost

Compound Assignment

u op= f where u is a uint256 and f is floating-point is equivalent to u = static_cast<uint256>(static_cast<Float>(u) op f), matching the built-in. The whole operation is performed in floating point and only the final result is converted back, truncating toward zero:

boost::int256::uint256 u {4U};
u *= 2.5;              // 10, not 8: the operand is not truncated first

Where the built-in would be undefined, that is when the floating-point result is negative, is NaN, or is above the range of the type, the library saturates instead, exactly as the floating-point constructor does.

f op= u where f is floating-point keeps the type of f and is equivalent to f op= static_cast<Float>(u).

Operators Not Provided

The built-in integers do not accept a floating-point operand for the modulo, bitwise, or shift operators, so neither does uint256. The following are declared as deleted, which makes them a compile error rather than silently converting the floating-point operand:

  • %, &, |, ^, <<, >> in either operand order,

  • %=, &=, |=, ^=, <<=, >>= with a floating-point right operand.

Precision of Comparisons

A comparison converts the 256-bit operand to the floating-point type before comparing, so it is only as precise as that type. This is the behavior of the built-in integers, but it matters far more at this width, since a 256-bit value needs far more bits than even a long double significand holds:

using boost::int256::uint256;

const uint256 v {(uint256{1U} << 200U) + uint256{1U}};  // 2^200 + 1

v == 1.6069380442589903e60;   // true: v rounds to 2^200 as a double

For a comparison that is exact for every value, use the integer comparison functions cmp_equal, cmp_less, and friends with an integer operand instead.

Limitations

  • __float128 is not supported, even on the platforms where the compiler provides it. Only float, double, long double, and the <stdfloat> extended types participate.

  • long double and the <stdfloat> extended types are unavailable on the CUDA and SYCL device, so their overloads are not declared when BOOST_INT256_HAS_GPU_SUPPORT is defined, matching the conversion operators.

  • A <stdfloat> extended type participates only when the current standard library actually ships it (checked per type, via its STDCPP_…​_T macro, never via __cpp_lib_stdfloat, which some standard libraries omit even when the header and the type both exist). As of this writing that is GCC 13 and later with libstdc; libc and MSVC’s standard library do not yet provide these types.

  • On a platform whose long double is IBM double-double, currently ppc64le, long double results are reproducible but not bitwise reproducible: that format is not canonical and its arithmetic is not correctly rounded, so two expressions computing the same product can differ in the last bits. float and double are unaffected, and so is every other long double format.