How to: accidentally break empty base optimization
Published on the
Topics: i-hate-c++
A reasonably common idiom in C++ code is the use of the following or similar class to inhibit move and copy operations, to avoid having to repeat the four delete
d functions in every class:
C++
class noncopyable {
public:
noncopyable(const noncopyable&) = delete;
noncopyable(noncopyable&&) = delete;
noncopyable& operator=(const noncopyable&) = delete;
noncopyable& operator=(noncopyable&&) = delete;
protected:
noncopyable() = default;
~noncopyable() = default;
};
I'm personally not a huge fan of the noncopyable
name, because it describes how instead of why, but that's neither here nor there.
The noncopyable
class can be mixed in using inheritance, like so:
C++
class foo : private noncopyable {
// …
};
So far, so good, but there's more to it than meets the eye.
The problem
This definition of noncopyable
relies heavily on empty base optimization: normally, every object in C++ has size of at least one byte and (equivalently) a unique address, but in certain conditions this requirement can be relaxed for empty base subobjects:
C++
class foo : private noncopyable {
int x;
};
// Always true.
static_assert(sizeof(noncopyable) >= 1);
// True on every implementation that doesn't add gratuitous padding after
// the last member.
static_assert(sizeof(foo) == sizeof(int));
There is a problem, however. Empty base optimization can be applied only if the base class type is not the type, or a possibly-indirect base type of that type, of the first member, as the two subobjects are explicitly required to have distinct addresses. From this requirement it follows that the empty base optimization is prohibited in the following code:
C++
class foo : private noncopyable {
int x;
};
class bar : private noncopyable {
foo x;
};
// Always true (!), as the noncopyable base subobject of foo and
// the noncopyable base subobject of bar are required to have distinct
// addresses.
static_assert(sizeof(bar) > sizeof(foo));
Now, if non-copyability of foo
is a part of its public API, one could argue that the solution is to just not declare bar
as non-copyable; but if it's merely an implementation detail, not explicitly making bar
non-copyable couples it too tightly to the implementation of foo
and as such should be, in my opinion at least, avoided.
The (temporary) workaround
There are ways to work around this problem. One such way is to use a preprocessor macro instead of mixing in a base class, but the C++ preprocessor is absolutely awful, so I'd rather not go this route.
The solution I personally prefer exploits the fact that one of the requirements for the empty base optimization to apply is that the base class is not of the same type as the first non-static data member or one of its bases. What if there was a way to create similar types that are completely distinct? There is, that's precisely what C++ templates are for:
C++
template <typename> // This is the only change!
class noncopyable {
public:
noncopyable(const noncopyable&) = delete;
noncopyable(noncopyable&&) = delete;
noncopyable& operator=(const noncopyable&) = delete;
noncopyable& operator=(noncopyable&&) = delete;
protected:
noncopyable() = default;
~noncopyable() = default;
};
As long as the template parameters T and U are distinct types, noncopyable<T>
and noncopyable<U>
are distinct types, too.
Making noncopyable
a template requires every type to somehow use a unique type as a parameter. Fortunately, there is an obvious solution: make every type use itself as the parameter:
C++
class foo : private noncopyable<foo> {
int x;
};
class bar : private noncopyable<bar> {
foo x;
};
// noncopyable<foo> and noncopyable<bar> are distinct types, so empty base
// optimization applies.
// True on every implementation that doesn't add gratuitous padding after
// the last member.
static_assert(sizeof(bar) == sizeof(foo));
Granted, this doesn't protect against types that deliberately use another class as the dummy parameter. This is a non-issue in practice, however, as any such code can be immediately caught and rejected at review stage. The goal is to protect against Murphy, not Machiavelli.
The (future) solution
C++20 solves this problem by allowing an arbitrary non-static data member to disable the unique address requirement through the [[no_unique_address]]
attribute, allowing noncopyable
to be used as a regular member instead of a base class:
C++
class foo {
[[no_unique_address]] noncopyable disable_copy_and_move;
int x;
};
// True on every implementation that doesn't add gratuitous padding after
// the last member.
static_assert(sizeof(foo) == sizeof(int));
An alternative, arguably semantically the cleanest, solution is to wait until the metaclass proposal (PDF, 1.5 MB) lands and implement noncopyable
as a metaclass. There's no guarantee however when (and if) it will land, while C++20 is coming soon, and GCC 9 and Clang 9 already implement [[no_unique_address]]
.