C++ / a working model

08 / 163   ·   C++11   ·   8 min

Endianness: separating numeric value from byte order

Keep this sentence

Endianness describes how the bytes of a multi-byte scalar are arranged in storage; it does not change the numeric value itself. A portable protocol should define field widths and encoding order explicitly, then encode and decode with unsigned arithmetic. Do not guess endianness by unaligned casts or by reading an inactive union member.

In this lesson
  1. Address order is not numeric order
  2. Query the platform, or avoid depending on it
  3. The example encodes on purpose; it does not copy a layout
  4. Example
  5. Exercise

Address order is not numeric order

On a platform with eight-bit bytes and the usual multi-byte integer representation, the big-endian representation of the value 0x12345678 is 12, 34, 56, 78 from low address to high address; little-endian reverses that sequence. They express the same numeric value. Arithmetic addition and right shift do not change their results because of endianness. The machine may store the bytes in either order; the value of an unsigned shift or a sum is still a value, not a picture of memory.

Endianness usually talks about bytes, not about reversing the bits inside a byte. The number of bits in a C++ byte is given by CHAR_BIT; an octet in network protocols is explicitly eight bits. When transmitting raw bytes you must check that premise and must not assume every platform has an eight-bit char. A discussion of big-endian versus little-endian is a discussion of how a multi-byte scalar occupies successive addresses, not a claim that arithmetic itself is stored backwards.

Query the platform, or avoid depending on it

C++20 <bit> provides std::endian. native may equal little or big, or it may equal neither, which represents a mixed case. Therefore a single if and else that assumes every implementation is either big or little is not a complete portable test. C++23 also provides integer byteswap to reverse byte order when you already have an object representation and genuinely need that reversal.

Much protocol code never needs to query the host endianness. Extract each eight-bit field with unsigned right shifts and masks, then store those fields in the specified order. On decode, widen the fields to a sufficiently wide unsigned type and shift them left. That describes rebuilding a value and does not depend on the host object representation. Prefer spelling the encoding in arithmetic over asking what the CPU happens to do with a native integer in memory.

The example encodes on purpose; it does not copy a layout

The example stores the value in an unsigned long that can hold at least thirty-two bits and produces four unsigned char elements in the range zero through two hundred fifty-five. Even if char has more than eight bits, those element values remain determinate. If they are truly written to an external device that accepts only eight-bit octets, the platform transport layer still has to meet the protocol. The lesson is the numeric encoding, not a dump of an unsigned long object representation.

You can inspect an object representation with character access or memcpy, but you must not cast a byte buffer and then dereference it as an integer: alignment, lifetime, and type-access rules can all go wrong. Floating-point values also involve representation format; swapping bytes alone does not yield a cross-platform universal floating-point encoding. Keep the numeric value in an unsigned type wide enough for the protocol, and emit or consume octets by shifts and masks rather than by overlaying a native integer on a buffer.

Pitfalls

  • Network byte order is usually big-endian, but a file format may specify something else; you must read the concrete protocol and cannot guess a file from network experience.
  • Reading the inactive other member of a union is not a portable endianness test in standard C++.

Run an example

Minimum C++11 · complete program · Download .cpp

#include <array>
#include <cassert>
#include <iostream>

int main() {
    const unsigned long value = 0x12345678UL;
    const std::array<unsigned char, 4> bytes = {{
        static_cast<unsigned char>((value >> 24) & 0xffUL),
        static_cast<unsigned char>((value >> 16) & 0xffUL),
        static_cast<unsigned char>((value >> 8) & 0xffUL),
        static_cast<unsigned char>(value & 0xffUL)
    }};
    unsigned long restored = 0;
    for (unsigned char b : bytes) restored = (restored << 8) | b;
    assert(restored == value);
    assert(bytes[0] == 0x12 && bytes[3] == 0x78);
    std::cout << static_cast<unsigned>(bytes[0]) << ' '
              << static_cast<unsigned>(bytes[3]) << '\n';
}

Compile locally

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread basics-endianness.cpp -o example && ./example

Expected result

18 120

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

A protocol's two octet fields are 0x01 then 0x02, encoding a big-endian unsigned integer. If the host is little-endian, how should you decode, and what is the result?

Show a reference answer

First ensure both field values are in 0..255, then use (static_cast<unsigned long>(first) << 8) | second; the result is 258. Because the expression operates on values rather than copying the host representation, a little-endian machine need not swap array elements first. If you memcpy directly into an integer, the result then depends on host endianness and on the size of the destination integer.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog