Byte Order Conversions
#include <boost/int128/byte_conversions.hpp>
The library provides functions for converting uint128 and int128 to and from big-endian or little-endian byte order, which is what serializing a 128-bit value into a file format, a network packet, or a database column requires.
Unlike the functions in <bit>, every one of these has both an unsigned and a signed overload: reversing a two’s complement bit pattern is the same operation for either sign.
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. On the platform whose byte order is being asked for they are the identity, and on the other they are abyteswap. The result ofto_beandto_leis a value whose object representation is in the requested order, so it is meant to be written out (or `memcpy’d) rather than read as a number. -
to_*bytes/from*_bytesoperate on 16-byte arrays. These never depend on the host byte order (with the sole exception of thenepair), 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.
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::int128::to_be_bytes(value)}; // std::array<std::uint8_t, 16>
const auto as_byte {boost::int128::to_be_bytes<std::byte>(value)}; // std::array<std::byte, 16>
const auto as_char {boost::int128::to_be_bytes<char>(value)}; // std::array<char, 16>
The number of bytes is fixed at sizeof(T), which is 16, 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 16 bytes from the address given. A std::span, std::vector, or any other contiguous range is passed by handing .data() to the pointer overload.
|
In a CUDA translation unit the pointer overloads of from_be_bytes, from_le_bytes, and from_ne_bytes are the portable choice. The std::array overloads read the array through std::array::operator[], which is a constexpr host function, so nvcc warns (#20013-D) unless it is given --expt-relaxed-constexpr. The warning appears even when the call itself is host code, because nvcc compiles the device side of a host device template as well. Writing bytes with to_be_bytes, to_le_bytes, or to_ne_bytes is unaffected: the returned array is constructed and assigned without calling any of its member functions.
|
to_be
Converts a value from the native byte order to big-endian byte order.
On a big-endian platform the value is returned unchanged, and on a little-endian platform byteswap is applied.
namespace boost {
namespace int128 {
BOOST_INT128_HOST_DEVICE constexpr uint128 to_be(uint128 value) noexcept;
BOOST_INT128_HOST_DEVICE constexpr int128 to_be(int128 value) noexcept;
} // namespace int128
} // namespace boost
from_be
Converts a value from big-endian byte order to the native byte order.
This is the inverse of to_be, and since a byte reversal is its own inverse it delegates directly to to_be.
namespace boost {
namespace int128 {
BOOST_INT128_HOST_DEVICE constexpr uint128 from_be(uint128 value) noexcept;
BOOST_INT128_HOST_DEVICE constexpr int128 from_be(int128 value) noexcept;
} // namespace int128
} // namespace boost
to_le
Converts a value from the native byte order to little-endian byte order.
On a little-endian platform the value is returned unchanged, and on a big-endian platform byteswap is applied.
namespace boost {
namespace int128 {
BOOST_INT128_HOST_DEVICE constexpr uint128 to_le(uint128 value) noexcept;
BOOST_INT128_HOST_DEVICE constexpr int128 to_le(int128 value) noexcept;
} // namespace int128
} // namespace boost
from_le
Converts a value from little-endian byte order to the native byte order.
This is the inverse of to_le, and delegates directly to it.
namespace boost {
namespace int128 {
BOOST_INT128_HOST_DEVICE constexpr uint128 from_le(uint128 value) noexcept;
BOOST_INT128_HOST_DEVICE constexpr int128 from_le(int128 value) noexcept;
} // namespace int128
} // namespace boost
to_be_bytes
Returns the 16 bytes of the value with the most significant byte first, on every platform.
namespace boost {
namespace int128 {
template <typename ByteType = std::uint8_t>
BOOST_INT128_HOST_DEVICE constexpr std::array<ByteType, sizeof(uint128)> to_be_bytes(uint128 value) noexcept;
template <typename ByteType = std::uint8_t>
BOOST_INT128_HOST_DEVICE constexpr std::array<ByteType, sizeof(int128)> to_be_bytes(int128 value) noexcept;
} // namespace int128
} // namespace boost
from_be_bytes
Reconstructs a value from 16 bytes in big-endian order.
The target type is given explicitly and must be uint128 or int128.
namespace boost {
namespace int128 {
template <typename T, typename ByteType, std::size_t N>
BOOST_INT128_HOST_DEVICE constexpr T from_be_bytes(const std::array<ByteType, N>& bytes) noexcept;
template <typename T, typename ByteType>
BOOST_INT128_HOST_DEVICE constexpr T from_be_bytes(const ByteType* bytes) noexcept;
} // namespace int128
} // 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 16 bytes of the value with the least significant byte first, on every platform.
namespace boost {
namespace int128 {
template <typename ByteType = std::uint8_t>
BOOST_INT128_HOST_DEVICE constexpr std::array<ByteType, sizeof(uint128)> to_le_bytes(uint128 value) noexcept;
template <typename ByteType = std::uint8_t>
BOOST_INT128_HOST_DEVICE constexpr std::array<ByteType, sizeof(int128)> to_le_bytes(int128 value) noexcept;
} // namespace int128
} // namespace boost
from_le_bytes
Reconstructs a value from 16 bytes in little-endian order.
The size requirements match from_be_bytes.
namespace boost {
namespace int128 {
template <typename T, typename ByteType, std::size_t N>
BOOST_INT128_HOST_DEVICE constexpr T from_le_bytes(const std::array<ByteType, N>& bytes) noexcept;
template <typename T, typename ByteType>
BOOST_INT128_HOST_DEVICE constexpr T from_le_bytes(const ByteType* bytes) noexcept;
} // namespace int128
} // namespace boost
to_ne_bytes
Returns the 16 bytes of the value in the native byte order, which is the object representation of the value.
Delegates to to_le_bytes on a little-endian platform and to to_be_bytes on a big-endian one, so this is the only byte-array function whose result varies across platforms.
namespace boost {
namespace int128 {
template <typename ByteType = std::uint8_t>
BOOST_INT128_HOST_DEVICE constexpr std::array<ByteType, sizeof(uint128)> to_ne_bytes(uint128 value) noexcept;
template <typename ByteType = std::uint8_t>
BOOST_INT128_HOST_DEVICE constexpr std::array<ByteType, sizeof(int128)> to_ne_bytes(int128 value) noexcept;
} // namespace int128
} // namespace boost
from_ne_bytes
Reconstructs a value from 16 bytes in the native byte order.
Delegates to from_le_bytes on a little-endian platform and to from_be_bytes on a big-endian one.
The size requirements match from_be_bytes.
namespace boost {
namespace int128 {
template <typename T, typename ByteType, std::size_t N>
BOOST_INT128_HOST_DEVICE constexpr T from_ne_bytes(const std::array<ByteType, N>& bytes) noexcept;
template <typename T, typename ByteType>
BOOST_INT128_HOST_DEVICE constexpr T from_ne_bytes(const ByteType* bytes) noexcept;
} // namespace int128
} // namespace boost
Examples
// Copyright 2026 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt
#include <boost/int128/int128.hpp>
#include <boost/int128/byte_conversions.hpp>
#include <boost/int128/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::int128::uint128;
using boost::int128::int128;
// The 16 bytes 01 02 ... 10 read as a big-endian value
constexpr uint128 value {UINT64_C(0x0102030405060708), UINT64_C(0x090A0B0C0D0E0F10)};
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::int128::to_be_bytes(value));
print_bytes("to_le_bytes:", boost::int128::to_le_bytes(value));
// Native order is whichever of the two matches the host, so this is the
// one form whose output depends on the platform
print_bytes("to_ne_bytes:", boost::int128::to_ne_bytes(value));
std::cout << "\n=== Reading a value back out of bytes ===" << std::endl;
constexpr std::array<std::uint8_t, sizeof(uint128)> wire
{{
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::int128::from_be_bytes<uint128>(wire) << std::endl;
std::cout << "from_le_bytes: " << boost::int128::from_le_bytes<uint128>(wire) << std::endl;
// Everything is constexpr, so a whole round-trip can be checked at compile time
static_assert(boost::int128::from_be_bytes<uint128>(boost::int128::to_be_bytes(value)) == value,
"Round trip through big-endian bytes");
std::cout << "\n=== Signed values ===" << std::endl;
// The two's complement bit pattern is what gets reversed, so negative
// values need no special handling
constexpr int128 negative {-300};
print_bytes("to_be_bytes(-300):", boost::int128::to_be_bytes(negative));
std::cout << "from_be_bytes: " << boost::int128::from_be_bytes<int128>(boost::int128::to_be_bytes(negative)) << 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::int128::to_be(value)};
print_bytes("object representation of to_be(value):", boost::int128::to_ne_bytes(big_endian_image));
std::cout << "from_be recovers: " << boost::int128::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::int128::to_le_bytes<char>(value)};
std::cout << "\nfrom_le_bytes over a char buffer: " << boost::int128::from_le_bytes<uint128>(as_char.data()) << std::endl;
return 0;
}
Output (on a little-endian host, where the to_ne_bytes lines match to_le_bytes):
=== Byte arrays === to_be_bytes: 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 to_le_bytes: 10 0f 0e 0d 0c 0b 0a 09 08 07 06 05 04 03 02 01 to_ne_bytes: 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: 58491224111394833235396041148664381440 === Signed values === to_be_bytes(-300): ff ff ff ff ff ff ff ff ff ff ff ff ff ff fe d4 from_be_bytes: -300 === 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 from_be recovers: 1339673755198158349044581307228491536 value: 1339673755198158349044581307228491536 from_le_bytes over a char buffer: 1339673755198158349044581307228491536