Olavi Kuusela

Packing structures in C++ without surprises

5 December 2025 · 6 min read

Every so often I have to write a structure whose layout matters: something that goes into a file, a protocol frame, or a shared memory region. The failure mode is always the same — the code compiles, the test passes on x86, and the file is unreadable on ARM.

Padding is not decoration

The compiler inserts padding so that every member sits at an address aligned to its natural alignment. This is not optional and it is not the compiler being wasteful:

struct Header {
    uint8_t  version;   // offset 0
    uint32_t length;    // offset 4, three bytes of padding after version
    uint16_t flags;     // offset 8
};                      // sizeof == 12, not 7

sizeof(Header) is twelve. Write it with fwrite and you write twelve bytes, including three that hold whatever was in memory before. If those bytes end up in a hash or a signature, the result is not reproducible; if they end up on a network, they are an information leak.

Three lines that catch it at compile time

static_assert(sizeof(Header) == 12);
static_assert(offsetof(Header, length) == 4);
static_assert(offsetof(Header, flags) == 8);

These cost nothing, they document the layout in a way a comment cannot, and they fail the build on any platform where the assumption stops holding. I put them under every structure that crosses a boundary. The offsetof checks are the ones that actually find problems; the size check alone will happily pass on a differently broken layout.

Why #pragma pack is usually the wrong answer

The tempting fix is to pack everything to one byte. It removes the padding and also removes the alignment guarantee: a uint32_t at offset 1 is misaligned, and on strict-alignment architectures reading it through the packed pointer is undefined behaviour, often a fault. Some compilers handle it, some generate two loads, some crash.

If the layout has to be exact, serialise explicitly: write each field into a byte buffer at a known offset, little-endian, one field at a time. It is a few more lines and it is correct everywhere, including on platforms you have never tested.

Endianness, the second surprise

Packing to one byte does not make the file portable. A little-endian host writes little-endian integers, and the file is wrong everywhere else. htole32 and friends exist for a reason; apply them at the boundary and nowhere else, so the rest of the program never has to think about it.

A rule I use

Any structure that leaves the process — file, socket, shared memory — gets an explicit serialiser, a version byte, and a static_assert on the size. Structures that never leave the process get nothing, because the compiler is better at layout than I am.

← All posts