Skip to main content

Padding and offsetof

This page is the low-level / ABI view: how padding affects a struct's on-the-wire byte layout, and how offsetof lets you address members by raw byte offset for serialization and memory-mapped I/O.

Prerequisite

The fundamentals โ€” what alignment is, why padding exists, how to order members to shrink a struct โ€” live on the canonical Memory Alignment page. The language-level alignof/alignas/offsetof reference is in Alignment and offsetof. This page assumes both.

Why layout matters at the ABI levelโ€‹

When you write() a struct to a socket or mmap a file onto one, you are betting that your byte layout matches the other side's. Padding is part of that layout โ€” and it is not guaranteed across compilers or architectures. Two rules keep you safe:

  1. Verify the layout you depend on with static_assert.
  2. Only treat standard-layout types as raw bytes.
struct NetworkPacket {
uint32_t magic; // offset 0
uint16_t version; // offset 4
uint16_t flags; // offset 6
uint32_t length; // offset 8
uint8_t data[1024]; // offset 12
};

static_assert(std::is_standard_layout_v<NetworkPacket>);
static_assert(offsetof(NetworkPacket, data) == 12); // pin the layout you rely on

void send(int sock, const NetworkPacket& p) {
write(sock, &p, sizeof p); // safe only because the asserts above hold
}

offsetof for byte-level accessโ€‹

offsetof(Type, member) yields the member's byte offset, letting you reach a field through a raw pointer โ€” the basis of serialization and protocol parsing.

#include <cstddef>

char* base = reinterpret_cast<char*>(&packet);
uint8_t* data_ptr = reinterpret_cast<uint8_t*>(base + offsetof(NetworkPacket, data));
Standard-layout only

offsetof on a type with virtual functions, multiple/virtual inheritance, or mixed-access members is undefined behaviour โ€” such types have no flat, portable offset table. See Object Layout for what the vtable pointer does to a class.

Bit-fields โ€” sub-byte packingโ€‹

Bit-fields pack several values into the bits of one integer. Useful for hardware registers and dense flags, but the bit order and straddling rules are implementation-defined โ€” never rely on the layout across compilers, and don't offsetof a bit-field (it's ill-formed).

struct Flags {
unsigned ready : 1;
unsigned mode : 2;
unsigned count : 5;
// packed into one byte, then padded to the type's width
};
sizeof(Flags); // 4 โ€” implementation-defined

Flexible array membersโ€‹

A trailing array of unspecified size lets one allocation hold a header plus a variable-length payload โ€” a common serialization trick. [] is the C99 flexible array member (well supported as an extension); [0] is the older GCC spelling.

struct Message {
uint32_t count;
uint32_t data[]; // occupies no space in sizeof(Message)
};

size_t n = 10;
auto* m = static_cast<Message*>(std::malloc(sizeof(Message) + n * sizeof(uint32_t)));
m->count = n;
m->data[5] = 42; // lives in the extra bytes allocated above

Inheritance does not reuse base paddingโ€‹

A derived class's members are placed after the base subobject's trailing padding, not inside it โ€” so deriving can grow a type more than you'd expect. (The exception is the empty base optimization, covered in Object Layout.)

struct Base { char c; /* 3 padding */ int i; }; // sizeof 8
struct Derived : Base { char c2; /* 3 padding */ }; // sizeof 12, not 9

Summaryโ€‹

  • A struct's byte layout โ€” padding included โ€” is what crosses the wire; pin it with static_assert.
  • Treat only standard-layout types as raw bytes; offsetof is UB otherwise.
  • Bit-fields pack sub-byte data but are implementation-defined โ€” not portable across compilers.
  • Flexible array members carry a variable payload in a single allocation.
  • Inheritance appends after base padding; it isn't reused (barring the empty base optimization).