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, or private) 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.