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_le operate on whole values. The result of to_be and to_le is a value whose object representation is in the requested order, so it is meant to be written out (or memcpy-ed) rather than read as a number.

  • to_*bytes / from*_bytes operate on 32-byte arrays. Apart from the ne pair these never depend on the host byte order, so to_be_bytes returns 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_bytes and their from_* counterparts are value encodings. They produce and consume the same bytes for the same value on every platform.

  • to_ne_bytes and from_ne_bytes are 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) and to_le(x) return a value whose object representation is the requested encoding, so memcpy of to_be(x) equals to_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

Example 1. This example demonstrates the byte order conversion functions, for both 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