C++ [[no_unique_address]] Attribute Explained

Introduced in C++20, the [[no_unique_address]] attribute allows compilers to optimize memory layout by permitting empty non-static data members to overlap in storage with other members or subobjects. In modern C++, it solves a long-standing limitation where composition of stateless utility classes introduced unnecessary padding and increased sizeof overhead. This article explains how the C++ object model handles empty types, why traditional composition incurs size penalties, and how [[no_unique_address]] eliminates these costs without resorting to artificial inheritance.

The Problem: The C++ Object Identity Rule

In C++, every distinct object must have a unique memory address so that pointers to two distinct objects can never compare equal. Because of this rule, even an empty type cannot have a size of zero:

struct Empty {};
static_assert(sizeof(Empty) >= 1); // Typically 1 byte

When an empty type is included as a member variable inside another class, the compiler must allocate at least one byte for it. Due to alignment constraints, this single byte often introduces padding, significantly inflating the overall size of the containing object:

struct Example {
    Empty e;   // 1 byte + 7 bytes padding (on 64-bit systems)
    void* ptr; // 8 bytes
};

static_assert(sizeof(Example) == 16); // 8 bytes wasted due to alignment

The Historical Workaround: Empty Base Optimization (EBO)

Before C++20, the language allowed an exception to the unique address rule only through inheritance, a mechanism known as the Empty Base Optimization (EBO). If a class derived from an empty base class, the base subobject was permitted to occupy zero bytes, provided it did not share the same type as the first non-static data member or another base subobject:

struct ExampleEBO : Empty {
    void* ptr; // 8 bytes
};

static_assert(sizeof(ExampleEBO) == 8); // Size is optimized

While effective, EBO polluted class hierarchies. Developers were forced to use inheritance—an "is-a" relationship—purely for memory optimization, even when composition—a "has-a" relationship—was conceptually appropriate.

The Role of [[no_unique_address]]

The [[no_unique_address]] attribute instructs the compiler that a data member does not require a distinct address from other non-static data members or base subobjects of the class. If the member is an empty class type, the compiler is free to allocate zero bytes of storage for it.

struct OptimizedExample {
    [[no_unique_address]] Empty e;
    void* ptr;
};

static_assert(sizeof(OptimizedExample) == 8);

By applying this attribute:

  1. Zero-Byte Allocation: The empty member does not consume storage.
  2. Padding Elimination: No alignment padding is added to accommodate the empty member.
  3. Address Sharing: The address of the empty member (&opt.e) can be identical to the address of opt.ptr or the enclosing object itself.

Common Use Cases

The primary use cases for [[no_unique_address]] occur in generic and template programming, where structures frequently hold user-supplied policies, allocators, or comparators that are often stateless:

  • Custom Allocators: Standard library containers like std::vector store an allocator instance. Most allocators (such as std::allocator) carry no state. Marking them with [[no_unique_address]] ensures they do not inflate the container's size.
  • Function Objects and Predicates: Objects holding stateless comparators (e.g., std::less<T>) or custom deleters in smart pointers can be held without a memory penalty.
  • Tag Types: Marker structs used for dispatching or metadata can be embedded directly without overhead.

Technical Constraints and Caveats

  • Same-Type Collision: If two consecutive members are of the exact same empty type, the compiler must assign them different addresses to preserve the rule that distinct subobjects of the same type cannot share an address.
  • Compiler Support and MSVC: While [[no_unique_address]] is standard C++20, the Microsoft Visual C++ (MSVC) compiler historically ignored the standard attribute to preserve binary compatibility (ABI). MSVC users must specify [[msvc::no_unique_address]] to enable this optimization on that toolchain.