In C++ an empty class is a non-union class type such that:

  • not has non-static data members
  • not has virtual functions
  • not has virtual base classes
  • not has non-empty base classes

It is possible to use the the unary trait std::is_empty<T> to check if a type T is empty.

The usage of empty functions

Empty classes can be particluar useful in different situations, especially in meta-programming implementing metafunction and type traits. For example:

#include <iostream>
#include <type_traits>

// Metafunction
template <int a, int b>
struct Add
{
    constexpr static int value = a + b;
};

int main()
{
    std::cout << "Is Add an empty calss: " << std::is_empty_v<Add<1, 1>> << std::endl;
}

They can be also useful in the policy-based class construction:

#include <iostream>
#include <type_traits>

struct NoFly
{
    void fly()
    {
        std::cout << "I can't fly!\n";
    }
};

struct FlyWithWings
{
    void fly()
    {
        std::cout << "I am flying!\n";
    }
};

template <typename FlyPolicy>
class Duck : public FlyPolicy
{
public:
    Duck() = default;
};

int main()
{
    Duck<FlyWithWings> realDuck;
    Duck<NoFly>        rubberDuck;

    realDuck.fly();   // I am flying!
    rubberDuck.fly(); // I can't fly!

    std::cout << std::boolalpha;

    std::cout << "Is NoFly an empty calss: " << std::is_empty_v<NoFly> << std::endl;
    std::cout << "Is FlyWithWings an empty calss: " << std::is_empty_v<FlyWithWings> << std::endl;
    std::cout << "Is Duck an empty calss: " << std::is_empty_v<Duck<FlyWithWings>> << std::endl;
}

EBO idiom

The C++ standard states that

An empty class must have non-zero size, but when derived or deriving it can have zero size.

Let's considerer the following example:

#include <iostream>

class EmptyClass1
{
};
class EmptyClass2
{
};

/* Deactivating EOB */

class PessimizedClass
{
    EmptyClass1 ec1;
    EmptyClass2 ec2;

    int data;
}; // sizeof(MyClass) = 8

/* Activating EOB */

class OptimizedClass : private EmptyClass1, EmptyClass2
{
    int data;
}; // sizeof(MyClass2) = 4

int main()
{
    std::cout << "sizeof(EmptyClass1) + sizeof(EmptyClass2) == "
              << sizeof(EmptyClass1) + sizeof(EmptyClass2) << std::endl;                // 2
    std::cout << "sizeof(PessimizedClass) == " << sizeof(PessimizedClass) << std::endl; // 8

    std::cout << "sizeof(OptimizedClass) == " << sizeof(OptimizedClass) << std::endl; // 4
}

It becomes clear that the size of EmptyClass is 1, while the PessimizedClass class includes both types as data members, so it has two extra bytes for padding satisfying alignment requirements. That is clearly a storage inefficiency. However, using the two EmptyClass types as base classes, the derived class OptimizedClass can access all their features without incurring storage costs.

Reference


Published

Category

Implementation details

Tags

Contact