Packing structures in C++ without surprises
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.