Design Decisions

Exactly 256 Bits on Every Platform

The central design goal is that sizeof(uint256) is exactly 32 bytes on every supported platform, that the type is trivially copyable and standard layout, and that its object representation is four 64-bit words and nothing else. No compiler offers a native 256-bit integer, and a multiprecision integer stores bookkeeping alongside the digits, so neither can be written straight into a packet, a file format, or a device buffer. See Comparison to Boost.Multiprecision for the tradeoffs relative to a multiprecision number.

Four Words in a Fixed Order

The single data member is std::uint64_t words[4], with words[0] the least significant word on every platform, including big-endian ones. The library does not reorder the words by endianness. There is no native 256-bit type to match layouts with, so an endian-dependent order would buy nothing and would double the amount of code that has to be tested; a fixed order also keeps the constant-evaluated paths and the runtime paths identical.

The consequence is that on a big-endian host the object representation of a uint256 is mixed-endian: the bytes inside each word are big-endian, while the words themselves ascend from least significant to most significant. That only matters for the native-order byte functions, and it is exactly what Byte Order Conversions documents: to_ne_bytes and from_ne_bytes are defined as the object representation, while to_be_bytes, to_le_bytes, from_be_bytes, and from_le_bytes are host-independent value encodings that produce the same bytes for the same value on every platform.

The alignment is the natural alignment of the member array, that is alignof(std::uint64_t), and the type is deliberately not over-aligned. C++14 has no aligned operator new, so an over-aligned element type would make std::vector<uint256> ill-behaved, and MSVC warns about over-aligned aggregates passed by value. The SIMD paths therefore load through std::memcpy into a vector local rather than casting a pointer to the object.

The Signed Type Reuses the Same Layout

int256 is not a second implementation: it is the same std::uint64_t words[4], interpreted as two’s complement instead of pure magnitude, with the sign living in bit 63 of words[3]. Reusing the layout means a uint256 and an int256 holding the same bit pattern convert between each other with a plain bit copy, in either direction, and it means every design decision above (the fixed word order, the lack of over-alignment, the intrinsics-with-a-portable-fallback structure) applies to int256 unchanged. `, `-`, `*`, `<<`, `+, --, and unary - are all computed on the bit image in the unsigned domain and then reinterpreted as signed, so none of them can trigger signed-overflow undefined behavior the way the equivalent expression on a built-in signed integer can; the type’s is_modulo is true for exactly this reason.

Division and remainder are the one place sign has to be handled explicitly, and they are handled by reducing to the unsigned case rather than by a parallel signed implementation: abs(lhs) and abs(rhs) are computed first (as uint256 magnitudes), the existing unsigned division runs on those magnitudes, and only the final quotient’s sign is flipped when the operands' signs differ, with the remainder taking the sign of the dividend. abs of BOOST_INT256_INT256_MIN has the bit pattern 2255 exactly, which is representable as a uint256 magnitude, so this reduction has no missing case; it is the same reduction the built-in __int128 division that Boost.Int128 wraps uses internally, but done here as library code operating on the four words directly. That is also what makes BOOST_INT256_INT256_MIN / -1 well-defined rather than undefined: the naive alternative, negating the dividend and dividing two positive magnitudes, would itself overflow when the dividend is BOOST_INT256_INT256_MIN (its magnitude is not representable as a positive int256), which is a mistake this design avoids by never forming that negation. The overflow is instead absorbed once, at the end, by the same two’s-complement wrap already used for the other signed arithmetic operators: the quotient wraps back to BOOST_INT256_INT256_MIN, and the remainder is 0.

See int256 for the complete operator reference and Mixed Type Operations for how int256 and uint256 interoperate.

No Dependencies

Nothing under include/boost/int256/ includes a header from another Boost library, or from any other third-party library, outside of two __has_include guarded integrations (Boost.Charconv in charconv.hpp and {fmt} in fmt_format.hpp). The library is usable in a project that has no Boost at all, and the whole of it can be reduced to one file with the amalgamation script in extra/.

Intrinsics with a Portable Fallback

Every operation has a portable implementation over the four words that is constexpr in C++14 and is always present. Faster paths are layered on top of it and are selected at compile time:

  • The compiler’s built-in 128-bit type, where it exists, is used as the intermediate for the 64x64 to 128 bit products and the 128 by 64 bit division steps. It is never used as the storage representation. BOOST_INT256_NO_BUILTIN_INT128 turns this off, which is also what the SYCL device pass does automatically because the device target has no such type.

  • On MSVC x64 the corresponding intrinsics are used instead: _umul128, __umulh, _udiv128, _addcarry_u64, and _subborrow_u64.

  • On x86-64 with GCC or Clang a divq instruction provides the 128 by 64 bit division step, and the ADX or classic carry intrinsics provide the addition chains.

  • On 32-bit targets, and anywhere else without a wide intermediate, the portable four-word paths run, with the 64 by 32 bit division chain replacing the 128 by 64 bit one.

  • Equality uses SSE2 or AVX2 where the target has it, loading through std::memcpy. BOOST_INT256_NO_SIMD turns that off.

Because a compiler intrinsic cannot be evaluated at compile time, every non-constexpr path sits behind a constant-evaluation check, so constant expressions always take the portable path. The result is that a value computed at compile time and the same value computed at run time agree bit for bit, on every platform and in either configuration.

Multiplication and Division

Multiplication is 64-bit-word schoolbook: six full 64x64 to 128 bit products plus four low-half products, with the column sums carried explicitly because a column can carry more than one.

Division dispatches on the number of significant words in the divisor:

  • One word: a chain of 128 by 64 bit divisions where the hardware provides one, and a chain of 64 by 32 bit divisions where it does not.

  • Two words: Knuth’s Algorithm D over 64-bit limbs, with a Moller and Granlund reciprocal 3 by 2 accelerator used at run time.

  • Three or four words: Knuth’s Algorithm D.

Algorithm D is used with 64-bit limbs rather than the 32-bit limbs a textbook implementation uses, which halves the number of quotient-digit estimates. The estimate itself needs a 128 by 64 bit division, which is the step the platform paths above provide. See References for the sources.

Division by zero is undefined behavior, exactly as it is for the built-in integer types, and is not checked.

C++14 and constexpr

The library targets C++14 as its minimum standard. Relaxed C++14 constexpr allows nearly all functionality to be evaluated at compile time, including division and parsing a literal, so the type can be used in constant expressions much like a built-in integer. Meeting the C++14 bar also satisfies the conceptual requirements of libraries such as Boost.Math and Boost.Random, which this library integrates with directly.

Emulating a Built-in Integer

To behave like a built-in integer in generic code, the type provides implicit conversions to and from the built-in integer and floating-point types. The single exception is operator bool, which is explicit to match the standard library convention and to avoid unintended participation in arithmetic and overload resolution.

Mixed-width and mixed-sign operations follow the usual arithmetic conversions of the language, so a signed operand is sign-extended into all four words before the operation, and uint256{0} < -1 is true for the same reason 0U < -1 is. A floating-point operand converts the 256-bit operand to the floating-point type first, again as the usual arithmetic conversions require, and the combinations a built-in integer rejects (%, &, |, ^, <<, >> with a floating-point operand) are declared deleted rather than left to the implicit conversions. See Mixed Type Operations for the complete result-type rules.

GPU Support

The type and many of the free functions are annotated with BOOST_INT256_HOST_DEVICE so they can be used unchanged in CUDA device code when the library is compiled with BOOST_INT256_ENABLE_CUDA, and in SYCL device code under BOOST_INT256_ENABLE_SYCL. The pieces that depend on host facilities are host-only by construction: std::hash, the iostream operators, to_string, the formatters, and the long double conversion. See Configuration Macros.