Empty Base Optimization
A compiler optimization that eliminates the storage overhead of empty base classes, enabling zero-cost policy-based design patterns in C++.
Empty Base Optimization (EBO)since C++98When a class inherits from a type with no non-static data members and no virtual functions, the compiler may assign that base a size of zero within the derived object's layout, eliminating the one-byte overhead the base would otherwise require as a standalone object.
Overview
Every complete object in C++ must occupy at least one byte of storage so that distinct objects have distinct addresses. An empty class instantiated independently therefore has sizeof equal to one:
struct Empty {};
static_assert(sizeof(Empty) == 1); // guaranteed by the standardWhen that same empty type appears as a base class, the compiler is permitted β though not required prior to C++20 β to fold the base's contribution to zero within the derived object's layout. This is the Empty Base Optimization.
struct Empty {};
struct WithMember {
Empty e; // stored as a member: Empty still needs its own address
int x;
// sizeof(WithMember) >= 8 (1 byte + 3 padding + 4 for int, typically)
};
struct WithBase : Empty {
int x;
// EBO applies: sizeof(WithBase) == sizeof(int) == 4
};
static_assert(sizeof(WithBase) == 4); // passes on GCC, Clang, MSVCThe optimization is not just an academic curiosity. Policy-based class design stores stateless objects β allocators, comparators, deleters, mutexes β as members or base classes of a container. Without EBO, each stateless policy costs at least one byte plus alignment padding. With EBO, the cost is zero.
EBO fires reliably on every mainstream compiler for the common case. What changed in C++20 is that [[no_unique_address]] (C++20) extended a mandatory variant to data members, not just base classes.
Mechanism and Constraints
EBO applies when all of the following hold:
- The base class is empty: no non-static data members, no virtual functions, no virtual base classes.
- The base class is not the same type as the first non-static data member of the derived class (the unique-address invariant must be preserved between the base sub-object and any member at the same offset).
- The base class is not
finalβ afinalclass cannot be inherited from at all. - The same empty type does not appear more than once in the inheritance chain as a direct base.
Multiple distinct empty bases can be combined without cost:
struct AllocPolicy {};
struct MutexPolicy {};
struct LoggerPolicy {};
template<
typename T,
typename Alloc = AllocPolicy,
typename Lock = MutexPolicy,
typename Logger = LoggerPolicy>
class Container : private Alloc, private Lock, private Logger {
T* data_;
std::size_t size_;
public:
// sizeof(Container<int>) == sizeof(T*) + sizeof(size_t)
// when all three policies are empty β EBO eliminates all three
};If any two policy types are the same type, EBO cannot apply to both instances. Two sub-objects of the same type at the same address would be indistinguishable.
Examples
Compressed Pair
The most important EBO pattern in library code is the compressed pair: a two-element holder that uses EBO to store empty types at zero cost. Standard library implementations apply this to std::unique_ptr (storing the deleter), std::vector (storing the allocator), and similar types.
#include <type_traits>
#include <utility>
// Primary: T1 is empty and non-final β inherit from it
template<typename T1, typename T2,
bool = std::is_empty_v<T1> && !std::is_final_v<T1>>
// ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
// std::is_empty: C++11 std::is_final: C++14
// _v helpers: C++17
struct CompressedPairImpl : private T1 {
T2 second;
template<typename A, typename B>
CompressedPairImpl(A&& a, B&& b)
: T1(std::forward<A>(a)), second(std::forward<B>(b)) {}
T1& first() noexcept { return *this; }
const T1& first() const noexcept { return *this; }
};
// Fallback: T1 is non-empty or final β store as member
template<typename T1, typename T2>
struct CompressedPairImpl<T1, T2, false> {
T1 first_;
T2 second;
template<typename A, typename B>
CompressedPairImpl(A&& a, B&& b)
: first_(std::forward<A>(a)), second(std::forward<B>(b)) {}
T1& first() noexcept { return first_; }
const T1& first() const noexcept { return first_; }
};
template<typename T1, typename T2>
using CompressedPair = CompressedPairImpl<T1, T2>;
// Demonstration
struct DefaultDeleter {
void operator()(int* p) const { delete p; }
};
struct StatefulDeleter {
void (*fn)(int*);
void operator()(int* p) const { fn(p); }
};
static_assert(sizeof(CompressedPair<DefaultDeleter, int*>) == sizeof(int*));
// DefaultDeleter is empty β EBO eliminates it
static_assert(sizeof(CompressedPair<StatefulDeleter, int*>) > sizeof(int*));
// StatefulDeleter holds a function pointer β must occupy spacePolicy-Based Design
#include <algorithm>
#include <string>
#include <vector>
#include <cctype>
struct CaseSensitiveCompare {
bool operator()(std::string_view a, std::string_view b) const noexcept {
return a < b;
}
};
struct CaseInsensitiveCompare {
bool operator()(std::string_view a, std::string_view b) const noexcept {
return std::lexicographical_compare(
a.begin(), a.end(), b.begin(), b.end(),
[](unsigned char x, unsigned char y) {
return std::tolower(x) < std::tolower(y);
});
}
};
template<typename Compare = CaseSensitiveCompare>
class SortedStringSet : private Compare {
std::vector<std::string> data_;
public:
void insert(std::string s) {
Compare& cmp = *this;
auto pos = std::lower_bound(data_.begin(), data_.end(), s, cmp);
if (pos == data_.end() || cmp(s, *pos))
data_.insert(pos, std::move(s));
}
bool contains(std::string_view s) const {
const Compare& cmp = *this;
auto pos = std::lower_bound(data_.begin(), data_.end(), s, cmp);
return pos != data_.end() && !cmp(s, *pos);
}
};
// sizeof(SortedStringSet<CaseSensitiveCompare>) == sizeof(std::vector<std::string>)
// The comparator contributes zero bytes β EBO in actionC++20: [[no_unique_address]]
C++20 extended the same zero-cost guarantee to data members via the [[no_unique_address]] attribute. This is often cleaner than inheritance for policy storage because it avoids the public/protected/private access complications and works with final types:
#include <memory>
template<typename T, typename Deleter = std::default_delete<T>>
class UniquePtr {
T* ptr_;
[[no_unique_address]] Deleter del_; // zero cost when Deleter is empty β C++20
public:
explicit UniquePtr(T* p, Deleter d = {}) noexcept
: ptr_(p), del_(std::move(d)) {}
~UniquePtr() { del_(ptr_); }
T* get() const noexcept { return ptr_; }
// sizeof(UniquePtr<int>) == sizeof(int*) when Deleter is std::default_delete<int>
};Caveat: MSVC suppressed [[no_unique_address]]'s effect in some ABI modes prior to VS 2022 17.6. If you target MSVC with strict binary-size requirements, verify with a static_assert.
Best Practices
Prefer [[no_unique_address]] over inheritance in C++20 codebases. It makes intent explicit, avoids is-a semantic confusion from inheritance, and works even when the policy type is final. Reserve the inheritance pattern for C++17 and earlier.
Gate EBO inheritance on is_empty and is_final. Always guard the specialisation that inherits:
// C++17
template<typename T>
inline constexpr bool ebo_eligible_v =
std::is_empty_v<T> && !std::is_final_v<T>;Without the is_final check, inheriting from a final type is ill-formed and produces a hard error.
Validate with static_assert. EBO is a quality-of-implementation property pre-C++20, not a language guarantee. Pin the expected size in a test:
static_assert(sizeof(SortedStringSet<>) == sizeof(std::vector<std::string>),
"EBO not firing β check compiler settings or policy type");Use private inheritance for policies. Policy base classes are implementation details. private inheritance prevents the derived class's users from accidentally accessing or converting to the policy type.
Common Pitfalls
Duplicating the same empty base type is ill-formed. Two direct bases of the identical type produce a compile error β it is not merely an EBO failure:
template<int N> struct Tag {};
struct Fine : Tag<0>, Tag<1> {}; // OK: distinct types, EBO applies to both
struct Broken : Tag<0>, Tag<0> {}; // error: duplicate base classIf you genuinely need two independent instances of the same stateless policy, wrap each in a distinct tag:
template<typename T, int Tag> struct PolicyHolder : T {};
struct MyClass : PolicyHolder<AllocPolicy, 0>, PolicyHolder<AllocPolicy, 1> {};ABI breaks when switching between member and base storage. Moving a field from a regular member to an EBO base (or adding [[no_unique_address]]) changes the class layout. Recompile all translation units that include the class definition; mixing old and new ABIs causes silent corruption.
EBO and the first-member rule. The compiler cannot overlap the base sub-object with the first data member when they share the same type β doing so would make two sub-objects with the same address and the same type, which C++ forbids. This means EBO silently fails in this one specific scenario rather than producing an error:
struct Tag {};
struct FailsEBO : Tag {
Tag member; // base and first member are the same type; EBO must not apply
int x;
// sizeof(FailsEBO) > sizeof(int): the Tag base must have a distinct address
};MSVC /Zc:ebo conformance flag. Older MSVC versions disabled EBO for multiply-inherited empty bases by default. If you compile on MSVC and inherit from several empty types, confirm EBO fires with a static_assert or verify /Zc:ebo is active.
EBO in standard library containers
Every major standard library uses EBO to keep container overhead minimal. std::vector, std::basic_string, std::unique_ptr, and std::shared_ptr all embed their allocators or deleters via compressed-pair or equivalent base-class tricks so that stateless defaults cost nothing.
A concrete view of std::unique_ptr's layout illustrates this. When the deleter is std::default_delete<T> (an empty type), the pointer itself is the only payload:
#include <memory>
// C++11
static_assert(sizeof(std::unique_ptr<int>) == sizeof(int*),
"unique_ptr with default_delete must be pointer-sized");
// A custom stateful deleter bloats the size:
struct LoggingDeleter {
FILE* log;
void operator()(int* p) const { std::fprintf(log, "delete\n"); delete p; }
};
// sizeof is now sizeof(int*) + sizeof(FILE*) + possible padding
static_assert(sizeof(std::unique_ptr<int, LoggingDeleter>) > sizeof(int*));std::vector stores its allocator alongside the end-pointer via a compressed pair in libc++ and libstdc++. Swapping in a stateful allocator therefore has a measurable per-object size cost β something to consider in hot-path data structures.
[[no_unique_address]] attribute (C++20)
[[no_unique_address]] brings the EBO guarantee to data members without inheritance. Prior to C++20, zero-cost member storage required the workaround of inheriting from the empty type. The attribute instructs the compiler that a non-static data member need not have a unique address and may be overlapped with other sub-objects.
#include <cstdio>
struct Logger {
void log(const char* msg) const { std::puts(msg); }
};
struct StatefulLogger {
FILE* file;
void log(const char* msg) const { std::fputs(msg, file); }
};
// C++20
struct TaskA {
[[no_unique_address]] Logger logger;
int value;
};
struct TaskB {
[[no_unique_address]] StatefulLogger logger;
int value;
};
static_assert(sizeof(TaskA) == sizeof(int),
"Logger is empty β [[no_unique_address]] eliminates its footprint");
static_assert(sizeof(TaskB) > sizeof(int),
"StatefulLogger carries a FILE* β must occupy space");The attribute does not affect the type's observable behaviour or constexpr-ness; it is purely a layout directive. On MSVC, the attribute was recognised but its layout effect was suppressed under the default ABI until VS 2022 17.7 (/Zc:nouniqueaddress). Always confirm with a static_assert when targeting MSVC.
Compressor idiom: wrapping allocators
The compressor idiom solves the common pattern of bundling an allocator with a pointer inside a single struct. Many containers store exactly one pointer and one allocator; the compressor makes both occupy only sizeof(pointer) bytes when the allocator is stateless.
#include <memory>
#include <type_traits>
#include <utility>
// C++17
template<typename Ptr, typename Alloc,
bool Compress = std::is_empty_v<Alloc> && !std::is_final_v<Alloc>>
struct AllocHolder;
// Compressed path: inherit from allocator
template<typename Ptr, typename Alloc>
struct AllocHolder<Ptr, Alloc, true> : private Alloc {
Ptr ptr;
AllocHolder() = default;
AllocHolder(Ptr p, Alloc a) : Alloc(std::move(a)), ptr(p) {}
Alloc& alloc() noexcept { return *this; }
const Alloc& alloc() const noexcept { return *this; }
};
// Fallback path: store allocator as a member
template<typename Ptr, typename Alloc>
struct AllocHolder<Ptr, Alloc, false> {
Ptr ptr;
Alloc alloc_;
AllocHolder() = default;
AllocHolder(Ptr p, Alloc a) : ptr(p), alloc_(std::move(a)) {}
Alloc& alloc() noexcept { return alloc_; }
const Alloc& alloc() const noexcept { return alloc_; }
};
// Verify compression fires for the default allocator
using DefaultHolder = AllocHolder<int*, std::allocator<int>>;
static_assert(sizeof(DefaultHolder) == sizeof(int*));This is the exact pattern used by libstdc++ and libc++ internally for std::vector and other sequence containers. C++20 code can replace the template specialisation with a single [[no_unique_address]] member.
EBO with multiple empty bases
EBO extends to any number of distinct empty base types simultaneously. Each unique empty base contributes zero bytes, so a policy class can freely inherit from several orthogonal policies without any size penalty.
// C++11
struct AllocPolicy { void* allocate(std::size_t n); };
struct LockPolicy { void lock(); void unlock(); };
struct TracingPolicy { void trace(const char*); };
template<
typename T,
typename Alloc = AllocPolicy,
typename Lock = LockPolicy,
typename Tracer = TracingPolicy>
class SafeQueue
: private Alloc
, private Lock
, private Tracer
{
T* data_;
std::size_t head_, tail_, cap_;
public:
// With all three policies empty, the layout is:
// sizeof(SafeQueue<int>) == sizeof(T*) + 3 * sizeof(size_t)
// Zero overhead from the three base classes.
};The constraint is that no two direct bases may be the same type. Attempting to inherit from the same empty type twice is ill-formed; use the PolicyHolder<T, N> wrapper from the Common Pitfalls section when you genuinely need two instances.
Compilers schedule empty base sub-objects before data members and then pack them together, so even with five or six empty bases the data members start at offset zero.
EBO and final classes
A final class cannot be used as a base class at all, so EBO is categorically unavailable for final empty types. The final specifier prevents inheritance regardless of whether the class is empty, making the compressed-pair trick fail at compile time.
struct EmptyFinal final {};
// error: cannot derive from 'final' base 'EmptyFinal'
// struct Derived : EmptyFinal { int x; };The correct guard is the ebo_eligible_v predicate shown in Best Practices. When the guard evaluates to false (because the type is final), the fallback specialisation stores the value as a regular member. In C++20, [[no_unique_address]] is the clean solution because it works with final types:
// C++20
struct FinishedPolicy final {
void act() const {}
};
struct Host {
[[no_unique_address]] FinishedPolicy policy; // zero-cost even though final
int value;
};
static_assert(sizeof(Host) == sizeof(int));This is a decisive advantage of [[no_unique_address]] over inheritance-based EBO.
Measuring with sizeof
sizeof is the canonical tool for verifying that EBO is actually firing. A static_assert placed in a header provides a compile-time regression test that catches the moment a compiler setting, policy change, or base-class modification silently defeats the optimisation.
#include <vector>
#include <string>
#include <memory>
// C++11 β baseline size checks
static_assert(sizeof(std::unique_ptr<int>) == sizeof(int*),
"default_delete must be compressed away");
// Confirm a custom empty policy adds nothing
struct NullMutex { void lock() {} void unlock() {} };
struct NullAlloc {
using value_type = int;
int* allocate(std::size_t n) { return new int[n]; }
void deallocate(int* p, std::size_t) { delete[] p; }
};
template<typename M = NullMutex, typename A = NullAlloc>
class Buffer : private M, private A {
int* data_;
std::size_t size_;
};
static_assert(sizeof(Buffer<>) == sizeof(int*) + sizeof(std::size_t),
"Both NullMutex and NullAlloc must be compressed");
// When you add a real mutex, size grows β that's expected and fine
#include <mutex>
static_assert(sizeof(Buffer<std::mutex>) > sizeof(Buffer<>),
"std::mutex is not empty");On MSVC, size results may differ from GCC/Clang if /Zc:ebo is not active. Run the same static_assert suite under all target compilers in CI.
EBO vs [[no_unique_address]]: when to use which
[[no_unique_address]] is the modern default; EBO inheritance is the compatibility path. The two mechanisms achieve the same layout goal but differ in availability, expressiveness, and compiler support.
#include <type_traits>
struct Comparator { bool operator()(int a, int b) const { return a < b; } };
// ββ C++17 approach: EBO inheritance ββββββββββββββββββββββββββββββββββββββββββ
// C++17
template<typename Cmp = Comparator>
class SetV17 : private Cmp {
// ...
};
// ββ C++20 approach: [[no_unique_address]] member ββββββββββββββββββββββββββββββ
// C++20
template<typename Cmp = Comparator>
class SetV20 {
[[no_unique_address]] Cmp cmp_;
// ...
};
static_assert(sizeof(SetV17<>) == sizeof(SetV20<>)); // same layout result| Criterion | EBO inheritance | [[no_unique_address]] |
|---|---|---|
| Standard version | C++98 (de facto), not mandated | C++20, mandatory |
Works with final types | No | Yes |
| Expresses "is-a" semantics | Yes (often unwanted) | No β purely compositional |
| MSVC default ABI | Requires /Zc:ebo on older versions | Requires VS 2022 17.7+ for effect |
| Two instances of same type | Needs PolicyHolder<T,N> wrapper | Two annotated members at different offsets |
| Requires specialisation boilerplate | Yes | No |
Rule of thumb: target C++20 or later β use [[no_unique_address]]. Target C++17 or earlier β use the compressed-pair inheritance pattern with is_empty_v && !is_final_v guards. Mixing both in a library header is acceptable as long as the C++20 path is gated on __cplusplus >= 202002L.
See Also
[[no_unique_address]]β C++20 attribute applying the same guarantee to data membersstd::is_empty<T>,std::is_final<T>β C++11/C++14 type traits for EBO guards- Compressed pair β the standard pattern for applying EBO in library containers
- Policy-based class design β primary use case for zero-cost stateless type storage