A small note on private inheritance and empty classes

June 27, 2019

Perhaps I wasn’t paying enough attention, but private inheritance never really made much sense to me. I always brushed it off as some obscure C++ feature that’s only used in a handful of projects across the world. However, turns out it can be pretty useful.

Technically, private inheritance can be described as anonymous composition. In simpler terms, private inheritance is a way to have a member variable that doesn’t have a name.

1
2
3
4
5
6
7
8
struct S : private std::string {
    const std::string &string() const {
        return static_cast<const std::string &>(*this);
    }
    size_t length() const {
        return std::string::size();
    }
};

In the code above, we define a structure S that has got a std::string value somewhere inside. This sample also shows how we’d access this value.

Of course, we could have used a member variable. This would probably be a better choice here since it would let us get rid of this cumbersome syntax. The only possible disadvantage of this approach is that you have to come up with a name for this variable, and we all know how difficult that is.

Alright, that wasn’t that useful or interesting. Let’s take a look at an actually meaningful example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
struct no_copy {
    no_copy() = default;
    ~no_copy() = default;

    // remove copying operations
    no_copy(const no_copy &) = delete;
    no_copy &operator=(const no_copy &) = delete;

    // allow move operations
    no_copy(no_copy &&) = default;
    no_copy &operator=(no_copy &&) = default;
};

This structure doesn’t contain any data, just prohibits copying by deleting the copy constructor and copy assignment operations. This class can be used in two ways:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct A {
    no_copy move_only;
    int some_data;
    int some_more_data;
};

struct B : private no_copy {
    int some_data;
    int some_more_data;
};

These two classes are identical in a sense that their objects can’t be copied and contain two int member variables.

Since no_copy doesn’t contain any data you’d think it wouldn’t take up any space. This would mean that the objects of types A and B would be unaffected by this no_copy thing and their memory layouts would be the same, right?

Nope, C++ has to guarantee that all member variables have distinct memory addresses. Because of that this move_only field is going to take up some space, making the objects of class A slightly larger than they need to be.

But what about B? There’s something in C++ called empty base optimization. Since you’re reading this, you can probably read so go read the page I linked. Here’s a condensed version: empty base objects don’t take up any space. Oh, and read this page too.

This optimization can be used not just by our little no_copy class (by the way, boost provides something called boost/core/noncopyable which might be better depending on your use case). This is what std::tuple is based on, for example. Keep in mind, though, that this optimization can be pretty brittle.

Another interesting example of a class that can benefit from this optimization is std::unique_ptr. In a lot of cases, the Deleter class doesn’t contain any data. In these cases, by storing this deleter as a private base, the std::unique_ptr object can, for all intents and purposes, be treated as if it was just a simple pointer. I’m talking about memory layout, of course.

This is particularly useful with C APIs, where you have dedicated functions that destroy the passed object and free the memory. Let’s take a look at how we could use a smart pointer to manage such “C” objects.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// functions provided by some C API
A*   A_init();
void A_free(A *);

int case1(){
    std::unique_ptr<A, std::function<void(A*)>> p(A_init(), A_free);
    // ...
    // do something with p
    // ...
    return sizeof(p); // a nice way of checking object sizes on godbolt.org btw
}

struct ADeleter {
    void operator()(A *a){ A_free(a); }
};

// using a dedicated Deleter class
int case2(){
    std::unique_ptr<A, ADeleter> p(A_init());
    // ...
    // again, do something with p
    // ...
    return sizeof(p);
}

// C++20, using a lambda as a deleter
int case3(){
    std::unique_ptr<A, decltype([](A *a){ A_free(a); })> p(A_init());
    // ...
    // again, do something with p
    // ...
    return sizeof(p);
}

As you can guess, case2 and case3 both return 8 (when using a 64-bit build, of course). But how big will the pointer be in case1? 40 bytes. That’s a lot of bytes for a single pointer, even a smart one. Perhaps using std::function is unnecessarily grotesque, but even if you use a simple function pointer, the resulting size will be 16 bytes, which is definitely less but still not perfect.

Hopefully, this post gave you some idea why private inheritance is a thing. Let’s sum up a little:

  1. Private inheritance is basically the same as having a member variable that doesn’t have a name. I guess that could be useful sometimes.
  2. Because of how C++ is organized, member variables always take up some space in the object memory layout. This is not the case with base class objects.