Byte Order Conversions
#include <boost/int256/byte_conversions.hpp>
The library provides functions for converting uint256 to and from big-endian or little-endian byte order, which is what serializing a 256-bit value into a file format, a network packet, or a database column requires, and what reading a 256-bit hash back as an integer requires.
All of these functions are constexpr, and are available using C++14 like the rest of the library.
There are two families:
-
to_be/from_be/to_le/from_leoperate on whole values. The result ofto_beandto_leis a value whose object representation is in the requested order, so it is meant to be written out (ormemcpy-ed) rather than read as a number. -
to_*bytes/from*_bytesoperate on 32-byte arrays. Apart from thenepair these never depend on the host byte order, soto_be_bytesreturns the most significant byte first on every platform.
The byte arrays are built with shifts rather than from the object representation, so the byte-array functions give identical results on little-endian and big-endian hosts.
Semantics on a Big-Endian Host
uint256 stores its four words in a fixed order, with words[0] least significant, on every platform (see Layout and Alignment).
On a big-endian host the object representation is therefore mixed-endian: the bytes inside each word are big-endian, while the words themselves ascend from least significant. That has three consequences, all of which the test suite asserts on a big-endian machine:
-
to_be_bytes,to_le_bytesand theirfrom_*counterparts are value encodings. They produce and consume the same bytes for the same value on every platform. -
to_ne_bytesandfrom_ne_bytesare the object representation, so on a big-endian host they are neither the big-endian nor the little-endian encoding. Use them only to copy a value around within one process. -
to_be(x)andto_le(x)return a value whose object representation is the requested encoding, somemcpyofto_be(x)equalsto_be_bytes(x)on every platform, which is what makes them usable for writing into a buffer.
Byte Array Element Type
The array functions are templated on the element type, which defaults to std::uint8_t.
The accepted types are char, signed char, unsigned char, and, when the standard library provides it (C++17), std::byte:
const auto bytes {boost::int256::to_be_bytes(value)}; // std::array<std::uint8_t, 32>
const auto as_byte {boost::int256::to_be_bytes<std::byte>(value)}; // std::array<std::byte, 32>
const auto as_char {boost::int256::to_be_bytes<char>(value)}; // std::array<char, 32>
The number of bytes is fixed at sizeof(uint256), which is 32, so the from_*_bytes functions do not need a length argument. The std::array overloads reject a mismatched size at compile time, and the pointer overloads read exactly 32 bytes from the address given. A std::span, std::vector, or any other contiguous range is passed by handing .data() to the pointer overload.
|
The std::array returning overloads are host only, because std::array is not usable from a device pass. Device code uses the raw array overloads shown below, which write into a ByteType (&out)[32], and the pointer overloads of the from_*_bytes family.
|
to_be
Converts a value from the native representation to big-endian byte order. The returned value is meant to be copied out, not read as a number.
namespace boost {
namespace int256 {
BOOST_INT256_HOST_DEVICE constexpr uint256 to_be(const uint256& value) noexcept;
} // namespace int256
} // namespace boost
from_be
Converts a value from big-endian byte order to the native representation.
This is the inverse of to_be, and since the transformation is its own inverse it delegates directly to to_be.
namespace boost {
namespace int256 {
BOOST_INT256_HOST_DEVICE constexpr uint256 from_be(const uint256& value) noexcept;
} // namespace int256
} // namespace boost
to_le
Converts a value from the native representation to little-endian byte order. On a little-endian platform the value is returned unchanged.
namespace boost {
namespace int256 {
BOOST_INT256_HOST_DEVICE constexpr uint256 to_le(const uint256& value) noexcept;
} // namespace int256
} // namespace boost
from_le
Converts a value from little-endian byte order to the native representation.
This is the inverse of to_le, and delegates directly to it.
namespace boost {
namespace int256 {
BOOST_INT256_HOST_DEVICE constexpr uint256 from_le(const uint256& value) noexcept;
} // namespace int256
} // namespace boost
to_be_bytes
Returns the 32 bytes of the value with the most significant byte first, on every platform.
namespace boost {
namespace int256 {
template <typename ByteType = std::uint8_t>
constexpr std::array<ByteType, sizeof(uint256)> to_be_bytes(const uint256& value) noexcept;
// Usable from a device pass
template <typename ByteType>
BOOST_INT256_HOST_DEVICE constexpr void to_be_bytes(const uint256& value, ByteType (&out)[sizeof(uint256)]) noexcept;
} // namespace int256
} // namespace boost
from_be_bytes
Reconstructs a value from 32 bytes in big-endian order.
The target type is given explicitly and must be uint256.
namespace boost {
namespace int256 {
template <typename T, typename ByteType, std::size_t N>
constexpr T from_be_bytes(const std::array<ByteType, N>& bytes) noexcept;
template <typename T, typename ByteType>
BOOST_INT256_HOST_DEVICE constexpr T from_be_bytes(const ByteType* bytes) noexcept;
} // namespace int256
} // namespace boost
The std::array overload requires N == sizeof(T), and any other size is a static_assert failure.
The pointer overload reads sizeof(T) bytes starting at bytes, and the caller is responsible for that many bytes being readable.
to_le_bytes
Returns the 32 bytes of the value with the least significant byte first, on every platform.
namespace boost {
namespace int256 {
template <typename ByteType = std::uint8_t>
constexpr std::array<ByteType, sizeof(uint256)> to_le_bytes(const uint256& value) noexcept;
template <typename ByteType>
BOOST_INT256_HOST_DEVICE constexpr void to_le_bytes(const uint256& value, ByteType (&out)[sizeof(uint256)]) noexcept;
} // namespace int256
} // namespace boost
from_le_bytes
Reconstructs a value from 32 bytes in little-endian order.
The size requirements match from_be_bytes.
namespace boost {
namespace int256 {
template <typename T, typename ByteType, std::size_t N>
constexpr T from_le_bytes(const std::array<ByteType, N>& bytes) noexcept;
template <typename T, typename ByteType>
BOOST_INT256_HOST_DEVICE constexpr T from_le_bytes(const ByteType* bytes) noexcept;
} // namespace int256
} // namespace boost
to_ne_bytes
Returns the 32 bytes of the object representation of the value, which is what a memcpy of the object would produce.
On a little-endian host this matches to_le_bytes; on a big-endian host it is mixed-endian, as described above, so this is the only byte-array function whose result varies across platforms.
namespace boost {
namespace int256 {
template <typename ByteType = std::uint8_t>
constexpr std::array<ByteType, sizeof(uint256)> to_ne_bytes(const uint256& value) noexcept;
template <typename ByteType>
BOOST_INT256_HOST_DEVICE constexpr void to_ne_bytes(const uint256& value, ByteType (&out)[sizeof(uint256)]) noexcept;
} // namespace int256
} // namespace boost
from_ne_bytes
Reconstructs a value from 32 bytes in the object representation, and is the inverse of to_ne_bytes on every platform.
The size requirements match from_be_bytes.
namespace boost {
namespace int256 {
template <typename T, typename ByteType, std::size_t N>
constexpr T from_ne_bytes(const std::array<ByteType, N>& bytes) noexcept;
template <typename T, typename ByteType>
BOOST_INT256_HOST_DEVICE constexpr T from_ne_bytes(const ByteType* bytes) noexcept;
} // namespace int256
} // namespace boost
Signed values
Every overload above has an int256 twin with the same shape: to_be, from_be, to_le, from_le, and the to_*bytes / from*_bytes array forms all accept int256 as well as uint256 (the static_assert messages in the header name both types).
The two’s complement bit pattern is exactly what gets byte-reversed, so a negative int256 needs no special handling: converting int256{-300} to big-endian bytes and back recovers -300 the same way converting a uint256 does.
Because uint256 and int256 share the same 32-byte layout, the same wire bytes can also be read back as either type; only the interpretation of bit 63 of the last word differs.
Examples
uint256 and int256// Copyright 2026 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt
#include <boost/int256/int256.hpp>
#include <boost/int256/byte_conversions.hpp>
#include <boost/int256/iostream.hpp>
#include <array>
#include <cstddef>
#include <cstdint>
#include <iomanip>
#include <iostream>
// Prints the bytes of an array in the order they are stored
template <typename Bytes>
void print_bytes(const char* label, const Bytes& bytes)
{
std::cout << label;
for (const auto byte : bytes)
{
std::cout << ' ' << std::hex << std::setfill('0') << std::setw(2) << static_cast<unsigned>(byte);
}
std::cout << std::dec << std::endl;
}
int main()
{
using boost::int256::uint256;
using boost::int256::int256;
// The 32 bytes 01 02 ... 20 read as a big-endian value, four words most
// significant first
constexpr uint256 value {UINT64_C(0x0102030405060708), UINT64_C(0x090A0B0C0D0E0F10),
UINT64_C(0x1112131415161718), UINT64_C(0x191A1B1C1D1E1F20)};
std::cout << "=== Byte arrays ===" << std::endl;
// The byte order of the array is the requested one on every platform
print_bytes("to_be_bytes:", boost::int256::to_be_bytes(value));
print_bytes("to_le_bytes:", boost::int256::to_le_bytes(value));
// Native order is the object representation of the value. On a little-endian host
// it matches to_le_bytes; on a big-endian host the bytes of each word are big-endian
// while the words themselves stay least significant first, because the word order
// of uint256 is fixed on every platform.
print_bytes("to_ne_bytes:", boost::int256::to_ne_bytes(value));
std::cout << "\n=== Reading a value back out of bytes ===" << std::endl;
constexpr std::array<std::uint8_t, sizeof(uint256)> wire
{{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x2C
}};
// The target type is given explicitly, and the byte count has to match
std::cout << "from_be_bytes: " << boost::int256::from_be_bytes<uint256>(wire) << std::endl;
std::cout << "from_le_bytes: " << boost::int256::from_le_bytes<uint256>(wire) << std::endl;
// Everything is constexpr, so a whole round trip can be checked at compile time
static_assert(boost::int256::from_be_bytes<uint256>(boost::int256::to_be_bytes(value)) == value,
"Round trip through big-endian bytes");
static_assert(boost::int256::from_le_bytes<uint256>(boost::int256::to_le_bytes(value)) == value,
"Round trip through little-endian bytes");
static_assert(boost::int256::from_ne_bytes<uint256>(boost::int256::to_ne_bytes(value)) == value,
"Round trip through native bytes");
std::cout << "\n=== Signed values ===" << std::endl;
// The two's complement bit pattern is what gets reversed, so a negative
// int256 needs no special handling: the sign bit is just bit 63 of the
// last byte written (or the first, for to_be_bytes).
constexpr int256 negative {-300};
print_bytes("to_be_bytes(-300):", boost::int256::to_be_bytes(negative));
std::cout << "from_be_bytes: "
<< boost::int256::from_be_bytes<int256>(boost::int256::to_be_bytes(negative)) << std::endl;
static_assert(boost::int256::from_be_bytes<int256>(boost::int256::to_be_bytes(negative)) == negative,
"Round trip through big-endian bytes (signed)");
// uint256 and int256 share the same 32-byte layout, so the same wire bytes
// convert to either type; only the interpretation of the top bit differs
constexpr std::array<std::uint8_t, sizeof(int256)> negative_one_wire
{{
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
}};
std::cout << "As uint256 (max): " << boost::int256::from_be_bytes<uint256>(negative_one_wire) << std::endl;
std::cout << "As int256 (-1): " << boost::int256::from_be_bytes<int256>(negative_one_wire) << std::endl;
std::cout << "\n=== Whole value conversions ===" << std::endl;
// to_be and to_le produce a value whose object representation is in the
// requested order, which is what a memcpy into a packet buffer wants.
// The value itself is only meaningful again after the matching from_be / from_le.
const auto big_endian_image {boost::int256::to_be(value)};
print_bytes("object representation of to_be(value):", boost::int256::to_ne_bytes(big_endian_image));
std::cout << "from_be recovers: " << boost::int256::from_be(big_endian_image) << std::endl;
std::cout << "value: " << value << std::endl;
// Any byte-like element type can be requested, which is convenient when the
// surrounding buffer is not made of std::uint8_t
const auto as_char {boost::int256::to_le_bytes<char>(value)};
std::cout << "\nfrom_le_bytes over a char buffer: "
<< boost::int256::from_le_bytes<uint256>(as_char.data()) << std::endl;
// A 256-bit hash is the obvious case: the digest arrives as 32 big-endian bytes
// and becomes an integer that can be compared, ordered, and reduced
constexpr std::array<std::uint8_t, 32> digest
{{
0xE3, 0xB0, 0xC4, 0x42, 0x98, 0xFC, 0x1C, 0x14,
0x9A, 0xFB, 0xF4, 0xC8, 0x99, 0x6F, 0xB9, 0x24,
0x27, 0xAE, 0x41, 0xE4, 0x64, 0x9B, 0x93, 0x4C,
0xA4, 0x95, 0x99, 0x1B, 0x78, 0x52, 0xB8, 0x55
}};
const auto digest_value {boost::int256::from_be_bytes<uint256>(digest)};
std::cout << "\nSHA-256 of the empty string as an integer:" << std::endl;
std::cout << " decimal: " << digest_value << std::endl;
std::cout << " hex: " << std::hex << digest_value << std::dec << std::endl;
std::cout << " mod 10^9 + 7: " << digest_value % 1000000007U << std::endl;
return 0;
}
Output (on a little-endian host, where the to_ne_bytes line matches to_le_bytes):
=== Byte arrays === to_be_bytes: 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 to_le_bytes: 20 1f 1e 1d 1c 1b 1a 19 18 17 16 15 14 13 12 11 10 0f 0e 0d 0c 0b 0a 09 08 07 06 05 04 03 02 01 to_ne_bytes: 20 1f 1e 1d 1c 1b 1a 19 18 17 16 15 14 13 12 11 10 0f 0e 0d 0c 0b 0a 09 08 07 06 05 04 03 02 01 === Reading a value back out of bytes === from_be_bytes: 300 from_le_bytes: 19903532184728499472755846345868977080796606098303847563239893857561361776640 === Signed values === to_be_bytes(-300): ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff fe d4 from_be_bytes: -300 As uint256 (max): 115792089237316195423570985008687907853269984665640564039457584007913129639935 As int256 (-1): -1 === Whole value conversions === object representation of to_be(value): 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 from_be recovers: 455867356320691211509944977504407603390036387149619137164185182714736811808 value: 455867356320691211509944977504407603390036387149619137164185182714736811808 from_le_bytes over a char buffer: 455867356320691211509944977504407603390036387149619137164185182714736811808 SHA-256 of the empty string as an integer: decimal: 102987336249554097029535212322581322789799900648198034993379397001115665086549 hex: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 mod 10^9 + 7: 353860983