Release Notes

Unreleased

Fixed

  • operator>> now sets failbit (and zeroes the target) when no digit could be extracted or the value does not fit, so while (is >> value) terminates on invalid input as it does for the builtin types. Previously the stream reported success and put every character back, which looped forever.

  • operator>> with std::hex treated a bare leading zero as a base prefix and did not count a consumed 0x prefix, so 0 left its digit in the stream, 0f read as 0 instead of 15, and 0x1fz left 1fz behind. Octal input no longer strips a leading zero either, since it is already a digit in base 8.

  • operator<< with std::showbase, and std::format or fmt::format with the #o alternate form, no longer prefix a zero (0x0 and 00 are now 0), matching the standard facilities.

  • gcd returned 1 for equal operands wider than 64 bits (for example gcd(2^64 + 1, 2^64 + 1)) because the loop dropped to the 64-bit helper while the high word of the result was still nonzero.

  • detail::from_chars left the output at zero when no digit could be parsed and divided by zero for base 0. It now leaves the output untouched on any failure and returns EINVAL for a base outside 2 to 36.

  • unsigned-integer % int128::min() divided by zero. abs(int128::min()) wraps back to int128::min() itself, since its magnitude (2^127) is not representable, so the signed comparison used to detect "divisor magnitude exceeds the built-in lhs" saw it as very negative instead of very large, and fell through to a modulo by the (zero) low word of that wrapped value. unsigned-integer / int128::min() and the corresponding signed-lhs overloads were already correct.

  • int128 /= uint128 (and the other 9 compound assignment operators) had no dedicated cross-type overload, so the uint128 right operand converted to int128 first, a bit copy that changes how the value is interpreted whenever its top bit is set. This disagreed with the built-in model, in which __int128 compound-assigned from unsigned __int128 always computes in the unsigned domain and converts only the final result back. All 10 compound assignment operators (|=, &=, ^=, +=, -=, *=, /=, %=, <<=, >>=) between int128 and a uint128 right operand are now provided; see Compound Assignment Between int128 and uint128.

  • operator<< with std::showbase printed a negative int128 in hexadecimal or octal as 0x-ff instead of -0xff: the base prefix was written in front of the whole buffer, including the sign, rather than between the sign and the digits.

  • operator<< ignored std::setw/std::setfill entirely; the requested width was never consumed and no padding was ever written. std::left, std::right, and std::internal are now honored exactly as they are for the built-in integer types, with std::internal placing the fill between the sign/base-prefix and the digits (std::setw(12) << std::internal << std::hex << std::showbase << int128{-255} now prints "-0x ff", not the unpadded "-0xff" it printed before). std::showpos now also prints a leading + for a non-negative int128 in decimal, matching the built-in signed types.

  • The quoted-string and macro forms of an int128 literal ("-170141183460469231731687303715884105728"_i128, BOOST_INT128_INT128_C(-170141183460469231731687303715884105728), and the equivalent hex spellings) now correctly parse to BOOST_INT128_INT128_MIN. These forms range-check the signed value directly (a magnitude up to 2127 is allowed when the sign is negative), unlike a raw unquoted _i128 literal, which never sees the sign at all (-X_i128 is unary minus applied to X_i128) and so can never reach MIN this way, exactly as int256’s _i256 already documents.

New: C++23 <stdfloat> support

Fixes #513. uint128 and int128 now construct from, convert to, and support mixed arithmetic, comparison, and compound assignment with the C++23 <stdfloat> extended floating-point types: std::float16_t, float32_t, float64_t, float128_t, and bfloat16_t. Each is available only when the current standard library actually ships that specific type (checked individually, never via __cpp_lib_stdfloat, which some standard libraries omit even when the header and the type both exist); as of this writing that means GCC 13 and later with libstdc, since libc and MSVC’s standard library do not yet provide these types. They are host only, matching long double. Conversions are correctly rounded exactly like the existing float/double/long double ones, so std::float16_t, the narrowest of the five (an 8-bit significand, no value above 65504 in magnitude), overflows to infinity past that point. See Conversions and Operations With Floating-Point Types.

New: byte order conversions

The new header <boost/int128/byte_conversions.hpp> converts uint128 and int128 to and from big-endian or little-endian byte order, which is what writing a 128-bit value into a packet, a file format, or a database column requires.

to_be and to_le return a value whose object representation is in the requested order, and from_be and from_le read one back. to_be_bytes, to_le_bytes, and to_ne_bytes return the 16 bytes as a std::array, and from_be_bytes, from_le_bytes, and from_ne_bytes reconstruct the value from either a std::array or a pointer to at least 16 bytes.

constexpr uint128 value {UINT64_C(0x0102030405060708), UINT64_C(0x090A0B0C0D0E0F10)};

const auto bytes {to_be_bytes(value)};    // 01 02 03 ... 10 on every platform
static_assert(from_be_bytes<uint128>(bytes) == value, "Round trip");

Apart from the two native-endian functions, none of these depend on the host byte order, and both signs are supported since reversing a two’s complement bit pattern is the same operation either way. Every function is constexpr and available from C++14. See Byte Order Conversions.

Fix: the conversion to floating point is now correctly rounded on every platform

Where the compiler provides no built-in 128-bit integer type, which is MSVC, the CUDA and SYCL device, and every 32-bit target, the conversion to float and double composed the value as high * 2^64 + low. That rounds the high word, the low word, and the sum, so the result landed one ulp away from the correctly rounded value for roughly one value in 150. The 128-bit value is now rounded once, to nearest with ties to even, so a value converts to the same bit pattern whether or not the compiler has a built-in 128-bit integer.

long double was already correct, because a significand of 64 bits or more holds both words exactly and leaves the addition as the only rounding.

Overflow is unchanged and now covered by tests: a value whose correctly rounded result is above the range of the target type becomes infinity, which for float means every 128-bit value above 2^128 - 2^103.

New: mixed operations with the built-in floating point types

int128 and uint128 now provide every operator that the built-in 128-bit integers provide with a floating point operand, in either operand order, for float, double, and long double. These previously did not compile: because the conversions to and from floating point are both implicit, an expression such as u * 2.5 was ambiguous rather than merely unsupported.

uint128 u {5};

auto a = u + 1.0;    // double, 6.0
auto b = 1.0F - u;   // float, -4.0F
bool c = u < 5.5;    // true

The 128-bit operand is converted to the floating point type first, per the usual arithmetic conversions, so every result is bit-for-bit identical to the corresponding built-in __int128 expression. See Operations With Floating-Point Types.

Breaking: compound assignment with a floating point operand no longer truncates it first

u op= f for +=, -=, *=, and /= now converts u to the floating point type, applies the operation, and converts the result back, which is what the built-in does. Previously the floating point operand was converted to a 128-bit integer first, which silently gave a different answer:

uint128 u {4};
u *= 2.5;   // now 10, as for a builtin; previously 8, because 2.5 became 2

uint128 v {3};
v /= 0.5;   // now 6; previously a division by zero, because 0.5 became 0

New: compound assignment with a built-in integer on the left

Every compound assignment operator now accepts a built-in integer on the left and int128 or uint128 on the right, for bool through long long as well as the compiler’s __int128 and unsigned __int128. These previously did not compile: because the conversions in both directions are implicit, an expression such as i |= u was ambiguous rather than merely unsupported.

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

int counter {12};
counter += int128{-5};    // 7

i op= v is equivalent to i = static_cast<decltype(i)>(i op v), so the operation is performed in the common type of the two operands and only the result is converted back, matching the built-in __int128. A right operand wider than the left operand is therefore never truncated first. See Compound Assignment onto a Built-in Integer.

Fix: the shift operators accept a built-in left operand of any width

i << v and i >> v, where i is a built-in integer and v is int128 or uint128, were ambiguous for any i that integral promotion leaves alone, which is int and every wider type. Only the types promoted to int had an overload. The result type is the promoted left operand, as it is for the built-in shift operators, so the count is taken from v and the value and width from i:

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

Breaking: a shift with a narrow unsigned left operand now yields int

The result type of i << v and i >> v is the left operand after integral promotion. A type of lesser rank than int promotes to int whenever int can represent all of its values, and that is true whether or not the type is unsigned, so bool, unsigned char, and unsigned short all promote to a signed int. These previously returned unsigned int, which disagreed with the built-in:

unsigned short s {1};

auto r = s << uint128{1};   // now int, as for (unsigned __int128); previously unsigned int

The value and bit pattern are unchanged, and the compound forms <<= and >>= are unaffected because the result is converted back to the type of the left operand either way. Only code that deduces the type of the result, or that relies on the wrap-around of the previously unsigned result, sees a difference. The signed narrow types (signed char, short) already returned int and are unchanged.

Breaking: the modulo, bitwise and shift operators reject a floating point operand

The built-in 128-bit integers do not accept a floating point operand for %, &, |, ^, <<, or >>, so these are now declared deleted for int128 and uint128. The binary forms were already ill-formed, as an ambiguity; the compound forms such as u %= 2.0 and u <<= 2.0 previously compiled and silently truncated the operand.

New: integer division with every rounding mode

<boost/int128/numeric.hpp> now provides the div_* family proposed for the standard library by wg21.link/p3724[P3724 (Integer division)], for both uint128 and int128. operator/ only rounds towards zero; these functions round away from zero, towards either infinity, and to nearest under each of the six tie-breaking rules, along with Euclidean division and its always non-negative remainder.

div_to_neg_inf(int128{-7}, int128{2});   // -4, where -7 / 2 is -3
div_ties_to_even(int128{-7}, int128{2}); // -4, breaking the tie at -3.5
rem_euclid(int128{-7}, int128{2});       // 1, where -7 % 2 is -1

const auto res = div_rem_to_pos_inf(x, y);   // res.quotient and res.remainder, one division

Each mode also has a div_rem_ form returning a div_result<T> with both halves from a single division, and there is a standalone rem_euclid. See Integer Division for the full list.

Breaking: int128_t and uint128_t are now int128 and uint128

In order to avoid possible future possible ambiguity the types have been renamed. For detailed discussion see this reddit post.

Breaking: int128::high is now std::uint64_t

int128 previously stored std::uint64_t low and std::int64_t high. Both words are now std::uint64_t; the value is still interpreted as two’s complement across the pair, and no bit of the representation moved.

The reason is codegen. When the two halves of the struct have different types, a compiler’s data-reference analysis cannot merge them into a single wide access, so loops over int128 are emitted with shuffles or scalarized outright. Measured with GCC 14 at -O3 -march=znver3 on r[i] = x[i] | y[i] over an std::array<int128, 128>:

before after

|, &, ^

96 instructions, 14 shuffles

24 instructions, 0 shuffles

~

74 instructions, 10 shuffles

20 instructions, 0 shuffles

Those are now exactly the numbers uint128 has always produced, since it already had uniform word types. Scalar code is unaffected or better: a + b drops from 20 instructions to 10, a << n from 17 to 12, and a >> n from 18 to 13.

What still works

  • The two-word constructor is unchanged and still takes its high word as std::int64_t, so int128{-1, 0} and int128{INT64_MIN, 0} compile and mean exactly what they did.

  • sizeof, alignof, and the in-memory byte order are unchanged. The type remains trivially copyable and standard layout, so anything that memcpy`s an `int128 is unaffected.

  • Every operator produces bit-identical results. Conversions to and from uint128 and the native __int128 are unchanged.

What to change

Code that reads the high member and depends on its signedness. The member is unsigned now, so a sign test on it silently becomes false:

// Before: true for negative values. Now always false.
if (value.high < 0) { ... }

// Use the accessor
if (value.signed_high() < 0) { ... }

// Or, usually clearer, compare the value
if (value < 0) { ... }

signed_high() is a new public member:

BOOST_INT128_HOST_DEVICE constexpr std::int64_t signed_high() const noexcept;

It is a pure reinterpretation of the stored bits, constexpr everywhere, and available on device. Passing high to something expecting a std::int64_t also needs it, since the conversion is now the other direction.

See Storage and signed_high for details.