C++ programming language: Empty base optimization

To guarantee that different objects of the same type have different addresses in memory, at least 1 byte is allocated even for empty objects (instances of the class with no members).

What a waste of memory!

Empty base optimization (EBO) is the convention used by the language to allow the size of an empty base subobject to be zero.

So any objects must have unique addresses, but there are some exceptions, and the following rules are applied:

  1. Instance of struct / class has the same as the address as the first member in its layout.
  2. Instance of derived class object has the same address as the base class object.

only for Standard layout type.

Just as the short addition, here is the criteria of types being considered as standard-layout:

  1. scalar types
  2. standard-layout class types (have no virtual functions and base classes, have the same access control for all non-static data members)
  3. arrays of such types
  4. cv-qualified versions of these types

Simply speaking, it means that order in memory of such types are predictable and they coincide with the layout in the code.

Let’s look at the examples below!

  1. Structure without members
  2. Structure containing a POD value
  3. Structure containing a POD value and derived from a similar one
  4. Structure containing a POD value and derived from the empty one
  5. Structure containing a POD value with multiple inheritance from empty base classes
  6. Structure with empty member and attribute /no_unique_address

Structure without members

struct A {};

std::cout << "Size of empty struct A = " << sizeof(A) << "\n";
Size of empty struct A = 1

Structure containing a POD value

struct B {
    int v;
};

std::cout << "Size of struct B equals size of its member int = " << sizeof(B) << "\n";
Size of struct B equals size of its member int = 4

Structure containing a POD value and derived from a similar one

<<B>>
struct C : B {
    int v;
};

std::cout << "Size of struct C equals size of two ints = " << sizeof(C) << "\n";
Size of struct B equals size of its member int = 4
Size of struct C equals size of two ints = 8

B laid out first and C memebers are laid out next

Structure containing a POD value and derived from the empty one

<<A>>
struct D : A {
    int v;
};

std::cout << "Size of struct D equals size of 1 int = " << sizeof(D) << "\n";
Size of empty struct A = 1
Size of struct D equals size of 1 int = 4

So this is an example where compiler can optimize out size of the empty base class object

Structure containing a POD value with multiple inheritance from empty base classes

<<A>>
class E {
};

struct F : A, E {
    int v;
};

std::cout << "Size of struct F equals size of 1 int = " << sizeof(F) << "\n";
Size of empty struct A = 1
Size of struct F equals size of 1 int = 4

But behavior, particularly in this case, and its implementation can be different across compilers. For example, MSVC will not optimize out base empty classes in case of multiple inheritance by default. But there is a __declspec(empty_bases) attribute for MSVC. With this attribute, it will work as in GCC and Clang.

If your base class has a virtual destructor, you have the additional cost of a (hidden) pointer.

Structure with empty member and attribute no_unique_address

Since C++ 20, the empty member subobjects are permitted to be optimized out just like the empty bases if they use the attribute [[no_unique_address]]. Taking the address of such member results in an address that may equal the address of some other member of the same object.

struct Empty {}; // empty class

struct X
{
    int i;
    [[no_unique_address]] Empty e;
};

int main()
{
    // the size of any object of empty class type is at least 1
    static_assert(sizeof(Empty) >= 1);

    // empty member optimized out:
    static_assert(sizeof(X) == sizeof(int));
}