Empty Base Class Optimization in C++ Explained

Empty Base Class Optimization (EBCO) is a compiler optimization technique in C++ that allows stateless parent classes to consume zero bytes of memory within a derived class layout. Under standard rules, every standalone object must have a size of at least one byte to guarantee a unique memory address. However, when an empty class serves as a base class rather than a member, EBCO permits the compiler to overlap the base subobject's address with the derived object's address, entirely avoiding memory bloat and alignment padding.

The Problem: Why Empty Classes Have Non-Zero Size

In C++, an empty struct or class—one containing no non-static data members, no virtual functions, and no virtual bases—still reports a size of at least one byte when evaluated with sizeof:

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

This non-zero size is mandatory so that two distinct instances of the same class occupy different memory addresses. If sizeof(Empty) were zero, an array of Empty instances would have elements residing at the exact same address, violating the fundamental pointer arithmetic rules of the language.

When such an empty class is used as a member variable (composition), alignment and padding rules can lead to substantial memory overhead:

struct WithMember {
    Empty e; // Takes 1 byte + 3 bytes padding
    int data; // Takes 4 bytes
};
// sizeof(WithMember) is typically 8 bytes

How EBCO Eliminates Space Costs

EBCO removes this overhead by altering how base subobjects are laid out. The C++ standard allows a base subobject to share its address with the derived class object or with another subobject, provided that they do not share the exact same type.

When a class inherits from an empty base:

struct Derived : Empty {
    int data; // Takes 4 bytes
};
// sizeof(Derived) is 4 bytes

The compiler places the Empty base subobject at offset 0 of Derived. Because Derived already contains a member (int data) with a well-defined address and size, the base class requires no independent byte to ensure address uniqueness. Consequently, the derived class incurs zero additional bytes of storage and zero alignment padding for the empty base.

Limitations and Edge Cases

EBCO cannot be applied if doing so would result in two subobjects of the same type sharing the same memory address. This occurs in two main scenarios:

  1. Duplicate Empty Bases: If a class derives from two different empty base classes that ultimately inherit from the same empty base (without virtual inheritance), or if it directly inherits multiple times from the same base type, the compiler must allocate space for at least one of them to prevent address collision.
  2. Same-Type Member and Base: If a derived class inherits from Empty and also contains a member variable of type Empty, the base class subobject and the member subobject cannot be placed at the same offset. The compiler must assign distinct addresses to each, forcing at least one to consume physical space.

Practical Applications and C++20 Evolution

EBCO has historically been vital for library implementation, particularly in generic and template metaprogramming:

  • Stateless Allocators and Deleters: Types like std::vector and std::unique_ptr often store stateless allocators or custom deleters. By inheriting from these stateless utilities internally, standard library implementations ensure that std::unique_ptr remains as compact as a raw pointer (typically 8 bytes on 64-bit systems).
  • Tag Dispatching and Mixins: Stateless policy classes and tag types can be mixed into a class hierarchy without adding memory footprint.

With C++20, the need to use inheritance purely for this optimization has been reduced through the introduction of the [[no_unique_address]] attribute. This attribute instructs the compiler to apply empty object optimization directly to non-static data members, allowing modern C++ code to achieve zero-cost composition without relying on base class workarounds:

struct ModernOptimized {
    [[no_unique_address]] Empty e;
    int data;
};
// sizeof(ModernOptimized) is 4 bytes in C++20