In-Depth Analysis of C++20 Features: Design Motivation and Application Strategies of [[no_unique_address]]

1. Introduction: The Mystery of Empty Type Space

In the object model of the C++ language, an “empty class”—that is, a type that contains no non-static member variables, virtual functions, or base classes—should theoretically have zero size. However, the C++ standard states:

Every complete object must have a unique address.

This means that even if the type is empty, the compiler must still reserve at least 1 byte of space for its instance. For example:

struct Empty {};
struct Foo {    int x;    Empty e;    int y;};
static_assert(sizeof(Empty) == 1);
std::cout << sizeof(Foo) << std::endl;

You might expect <span>sizeof(Foo)</span> to equal <span>8</span>, but in most cases, it will be <span>12</span><span> (depending on the compiler's alignment).</span> This is not the compiler “wasting space” but rather a result of the standard semantics: each sub-object must have a different address.

This behavior can be particularly troubling in generic programming or template library design. When a class template introduces “policy classes,” “allocators,” or “tag types” as empty types through type parameters, a large number of instantiations can lead to unnecessary memory waste.

2. Historical Background: EBO (Empty Base Optimization)

Before the introduction of <span>[[no_unique_address]]</span>, the C++ community had already recognized the importance of optimizing empty types.

The compiler has long supported a mechanism known as Empty Base Optimization (EBO). When an empty class is used as a base class, the compiler is allowed (but not required) to make it occupy no space:

struct Empty {};
struct Derived : Empty {    int data;};
static_assert(sizeof(Derived) == 4); 

EBO is heavily utilized in the STL, for example in<span>std::allocator</span>, <span>std::char_traits</span>, <span>std::less</span>, and other empty base classes.

However, the applicability of EBO is extremely limited:

  • It only works in inheritance scenarios;

  • It is ineffective for member variables;

  • It has implementation limitations in multiple inheritance and virtual inheritance.

As a result, library authors were forced to write a lot of “tricks” for EBO templates:

template &lt;typename T, bool = std::is_empty_v&lt;T&gt;&gt;struct EboWrapper;
template &lt;typename T&gt;struct EboWrapper&lt;T, true&gt; : T {    EboWrapper() = default;    EboWrapper(const T&amp; t) : T(t) {}    T&amp; get() { return *this; }};
template &lt;typename T&gt;struct EboWrapper&lt;T, false&gt; {    T value;    T&amp; get() { return value; }};

Although this implementation achieves space folding, it disrupts class layout intuitiveness and leads to more complex inheritance hierarchies.3. A Unified Solution in C++20

C++20 formally introduces the <span>[[no_unique_address]]</span> attribute, which addresses the limitations of EBO from a semantic perspective. This attribute allows developers to explicitly declare:

“This member does not need to guarantee a unique address; the compiler is free to perform layout optimizations.”

Example:

struct Empty {};
struct Foo {    int a;    [[no_unique_address]] Empty e;    int b;};
static_assert(sizeof(Foo) == 8);

In the above example, the compiler can overlap or reuse the address of <span>Empty e</span> with other members, thus eliminating space waste for empty members.

4. Underlying Semantics and ABI Implications

The core definition of <span>[[no_unique_address]]</span> in the standard can be simplified to:

If a member is marked as <span>[[no_unique_address]]</span>, then the sub-objects of that member do not require a unique address within the same complete object.

In other words:

  • The compiler can co-locate this member with other members or empty base classes;

  • The address of this member may equal the address of the host object or other empty members;

  • <span>&obj.member</span> no longer serves as a unique identifier for the object.

Therefore, after marking this attribute, the behavior of the following code is “implementation-defined”:

struct Empty {};
struct Foo {    [[no_unique_address]] Empty e1;    [[no_unique_address]] Empty e2;};
Foo f;assert(&amp;f.e1 == &amp;f.e2); // may be true

This semantics breaks the traditional assumption that “sub-objects have independent addresses,” but it brings extreme space efficiency. Libraries with strict requirements for ABI stability (such as standard library implementations) must handle address dependencies carefully when using this attribute.5. Practical Case 1: Optimizing Empty Allocators in ContainersAlmost all C++ standard library containers hold an allocator member, for example:

template &lt;typename T, typename Alloc = std::allocator&lt;T&gt;&gt;struct Vector {    T* data;    std::size_t size, capacity;    Alloc alloc;};

If <span>Alloc</span> is an empty type, it still occupies space. The C++20 implementation can directly adopt:

template &lt;typename T, typename Alloc = std::allocator&lt;T&gt;&gt;struct Vector {    T* data;    std::size_t size, capacity;    [[no_unique_address]] Alloc alloc;};

This can directly save the size of a pointer (typically 8 bytes on a 64-bit system), which has a significant impact on the memory footprint of many small containers. GCC, Clang, and MSVC have all applied this feature in their standard library implementations.6. Practical Case 2:<span><span>std::optional</span></span> Zero-Space Encapsulation Suppose we implement a simplified version of <span>Optional</span>:

template &lt;typename T&gt;struct Optional {    bool has_value;    T value;};

If <span>T</span> is an empty type, <span>sizeof(Optional<T>)</span> will be at least 1 + alignment padding. After applying <span>[[no_unique_address]]</span>, it becomes:

template &lt;typename T&gt;struct Optional {    bool has_value;    [[no_unique_address]] T value;};
static_assert(sizeof(Optional&lt;std::tuple&lt;&gt;&gt;) == 1);

This optimization is one of the core space strategies for components like C++20 standard library <span>std::optional</span>, <span>std::variant</span>, and <span>std::tuple</span>.

Leave a Comment