Release Notes
Unreleased
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, soint128{-1, 0}andint128{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 thatmemcpy`s an `int128is unaffected. -
Every operator produces bit-identical results. Conversions to and from
uint128and the native__int128are 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.