C++ / a working model

30 / 163   ·   C++20   ·   8 min

Empty Classes, EBO and [[no_unique_address]]

Keep this sentence

A complete object of an empty class still has non-zero size, to support object identity and array addressing; an empty base-class subobject may occupy no extra space. C++20’s no_unique_address extends the opportunity for overlapping layout to members, but does not guarantee a particular sizeof, nor does it cancel the identity rules for objects of the same type.

In this lesson
  1. Empty Means State, Not Type Semantics
  2. Empty-Base Optimization and the Expressiveness of Composition
  3. Allowing Overlap Does Not Guarantee Compression
  4. Example
  5. Exercise

Empty Means State, Not Type Semantics

A class with no non-static data members may still have member functions, static data, type aliases and constructor/destructor behavior. For an ordinary empty class the sizeof of a complete object is at least one; it is not guaranteed to equal one. Adjacent elements in an array must be addressable as distinct objects, so a set of empty objects cannot all be collapsed into the same array element.

A class with virtual functions, even if it has no explicit data fields, does not belong to the empty types ordinarily used for empty-object optimization. The standard type trait is_empty does not judge whether an int field was written in the source. Empty policy types are often used as comparators, allocation policies and compile-time tags; they carry no state yet have practical use.

Empty-Base Optimization and the Expressiveness of Composition

An empty base-class subobject is not subject to the same minimum-size constraint as an ordinary complete object; an implementation may, under permitted layouts, give it no extra space—this is empty-base optimization (EBO). Certain standard-layout conditions also impose requirements on address relationships, but not every inheritance structure can be reduced to a fixed size equation.

If the reuse relationship is merely “has a policy”, using inheritance solely to save space for an empty member easily distorts the interface. C++20’s [[no_unique_address]] can mark a non-static non-bit-field member so that it becomes a potentially overlapping subobject; an empty policy therefore has a chance to share an address with other members while still keeping a clear composition relationship.

Allowing Overlap Does Not Guarantee Compression

The attribute lets an implementation exploit a layout opportunity; it does not promise a specified object size, nor guarantee that the member will share an address with another member. It may also allow reuse of trailing padding of a non-empty member, so one must not treat the entire sizeof-byte region of that member as exclusive storage that can be overwritten at will.

Two empty members of the same type inside one object still have to satisfy the address-distinction rule; they cannot be regarded as completely coincident merely because both carry the attribute. The example only asserts that an empty complete object is non-zero, that empty members of the same type have different addresses, and that the policy call is correct; it does not assert that the outer object must equal the size of int. Space savings should be measured on the target compiler and ABI and recorded as an implementation result.

Pitfalls

  • sizeof(Empty) being one is only a common result; alignment requirements and other factors can make it larger, so cross-platform code should not hard-code it.
  • no_unique_address does not extend lifetime, nor does it let you bypass object rules to overwrite shared storage; it is a layout attribute, not a license for manual memory management.

Run an example

Minimum C++20 · complete program · Download .cpp

#include <cassert>
#include <type_traits>

struct Empty {};
struct Identity {
    int operator()(int value) const { return value; }
};
struct ViaBase : Empty {
    int value = 3;
};
struct WithPolicy {
    [[no_unique_address]] Identity policy;
    int value = 7;
    int evaluate() const { return policy(value); }
};
struct TwoTags {
    [[no_unique_address]] Empty first;
    [[no_unique_address]] Empty second;
    int value = 1;
};

int main() {
    static_assert(std::is_empty_v<Empty>);
    static_assert(sizeof(Empty) >= 1);
    Empty array[2];
    assert(&array[0] != &array[1]);
    ViaBase inherited;
    WithPolicy composed;
    TwoTags tags;
    assert(inherited.value == 3);
    assert(composed.evaluate() == 7);
    assert(&tags.first != &tags.second);
}

Compile locally

g++ -std=c++20 -Wall -Wextra -Wpedantic -pthread objects-empty-class.cpp -o example && ./example

Expected result

Expected: exit 0, no output; every assert holds.

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

If sizeof(WithPolicy) equals sizeof(int) on some compiler, can that be written as a cross-platform static_assert in a library?

Show a reference answer

It should not. The observation shows that this particular implementation exploited empty-member overlap, but the attribute permits rather than mandates that concrete layout. A cross-platform assertion should constrain real semantics, such as the result of the policy call; if the library truly has a strict ABI size requirement, the supported platforms, compilers and build configurations should be limited and the size check treated as a specialized contract for that platform, not a general C++20 guarantee.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog