C++ Object Layout Inspection with offsetof
The offsetof macro in C++ provides a compile-time
mechanism to determine the byte offset of a specific data member from
the beginning of its enclosing class or structure. Inherited from the C
standard library via the <cstddef> header,
offsetof allows developers to inspect memory layout,
diagnose structure padding, and interface safely with low-level systems.
This article examines how the offsetof macro operates, the
strict type constraints modern C++ imposes on its use, and how it can be
combined with other language features to analyze memory
representation.
Syntax and Core Mechanics
The macro is defined in <cstddef> and takes two
arguments:
#include <cstddef>
offsetof(type, member-designator)It expands to an integral constant expression of type
std::size_t. The returned value represents the number of
bytes between the start of the object of type type and the
initial byte of the specified member-designator.
Historically in C, this was often implemented via pointer arithmetic
on a null pointer (e.g.,
(size_t)&(((type*)0)->member)). In modern C++,
invoking member access on a null pointer is undefined behavior.
Consequently, modern C++ compilers implement offsetof
through compiler intrinsics—such as GCC and Clang's
__builtin_offsetof—to compute offsets safely at compile
time without runtime execution or invalid dereferences.
Requirements: Standard-Layout Types
C++ imposes strict constraints on the types compatible with
offsetof. While C allows offsetof on any
struct or union, C++ restricts its
standard-compliant use to standard-layout types.
A class or struct qualifies as a standard-layout type if it satisfies the following conditions:
- It has no virtual functions and no virtual base classes.
- It has the same access control (
public,protected, orprivate) for all non-static data members. - All non-static data members and base classes are themselves standard-layout types.
- It has either no base classes with non-static data members, or has no non-static data members in the most derived class and at most one base class with non-static data members.
- It has no base classes of the same type as the first non-static data member.
You can verify standard-layout compliance at compile time using the
type trait std::is_standard_layout_v<T> from
<type_traits>.
Using offsetof on a non-standard-layout class (such as a
class with virtual member functions or mixed public and
private fields) results in undefined behavior, though some
compilers support it as an implementation-defined extension.
Practical Layout Inspection
offsetof is commonly used alongside sizeof
and alignof to determine how a compiler pads fields to
satisfy CPU alignment constraints.
#include <iostream>
#include <cstddef>
#include <type_traits>
struct PacketHeader {
char protocol_id; // 1 byte
// 3 bytes padding (assuming 4-byte alignment for int)
int sequence_num; // 4 bytes
char flags; // 1 byte
// 7 bytes padding (assuming 8-byte alignment for double)
double timestamp; // 8 bytes
};
static_assert(std::is_standard_layout_v<PacketHeader>,
"PacketHeader must be standard-layout to use offsetof");
int main() {
std::cout << "protocol_id: " << offsetof(PacketHeader, protocol_id) << " bytes\n";
std::cout << "sequence_num: " << offsetof(PacketHeader, sequence_num) << " bytes\n";
std::cout << "flags: " << offsetof(PacketHeader, flags) << " bytes\n";
std::cout << "timestamp: " << offsetof(PacketHeader, timestamp) << " bytes\n";
std::cout << "Total size: " << sizeof(PacketHeader) << " bytes\n";
return 0;
}In this example, the output reveals the exact byte offsets, showing
the gaps introduced by alignment requirements. Because the expressions
are compile-time constants, they can also be used in
static_assert declarations to enforce binary protocol
layouts or serialization invariants across different compilation
targets.