Empty base optimization is great. However, it comes with the following restriction:
Empty base optimization is prohibited if one of the empty base classes is also the type or the base of the type of the first non-static data member, since the two base subobjects of the same type are required to have different addresses within the object representation of the most derived type.
To explain this restriction, consider the following code. The static_assert
will fail. Whereas, changing either Foo
or Bar
to instead inherit from Base2
will avert the error:
#include <cstddef>
struct Base {};
struct Base2 {};
struct Foo : Base {};
struct Bar : Base {
Foo foo;
};
static_assert(offsetof(Bar,foo)==0,"Error!");
I understand this behavior completely. What I do not understand is why this particular behavior exists. It was obviously added in for a reason, since it is an explicit addition, not an oversight. What is a rationale for this?
In-particular, why should the two base subobjects be required to have different addresses? In the above, Bar
is a type and foo
is a member variable of that type. I don't see why the base class of Bar
matters to the base class of the type of foo
, or vice-versa.
Indeed, I if anything, I would expect that &foo
is the same as the address of the Bar
instance containing it—as it is required to be in other situations (1). After-all, I'm not doing anything fancy with virtual
inheritance, the base classes are empty regardless, and the compilation with Base2
shows that nothing breaks in this particular case.
But clearly this reasoning is incorrect somehow, and there are other situations where this limitation would be required.
Let's say answers should be for C++11 or newer (I'm currently using C++17).
(1) Note: EBO got upgraded in C++11, and in-particular became mandatory for StandardLayoutType
s (though Bar
, above, is not a StandardLayoutType
).
Ok, it seems as if I had it wrong all the time, since for all my examples there need to exist a vtable for the base object, which would prevent empty base optimization to start with. I will let the examples stand since I think they give some interesting examples of why unique addresses are normally a good thing to have.
Having studied this whole more in depth, there is no technical reason for empty base class optimization to be disabled when the first member is of the same type as the empty base class. This just a property of the current C++ object model.
But with C++20 there will a new attribute [[no_unique_address]]
that tells the compiler that a non-static data member may not need a unique address (technically speaking it is potentially overlapping [intro.object]/7).
This implies that (emphasis mine)
The non-static data member can share the address of another non-static data member or that of a base class, [...]
hence one can "reactivate" the empty base class optimization by giving the first data member the attribute [[no_unique_address]]
. I added an example here that shows how this (and all other cases I could think of) works.
Wrong examples of problems through this
Since it seems that an empty class may not have virtual methods, let me add a third example:
int stupid_method(Base *b) {
if( dynamic_cast<Foo*>(b) ) return 0;
if( dynamic_cast<Bar*>(b) ) return 1;
return 2;
}
Bar b;
stupid_method(&b); // Would expect 0
stupid_method(&b.foo); //Would expect 1
But the last two calls are the same.
Old examples (Probably don't answer the question since empty classes may not contain virtual methods, it seems)
Consider in your code above (with added virtual destructors) the following example
void delBase(Base *b) {
delete b;
}
Bar *b = new Bar;
delBase(b); // One would expect this to be absolutely fine.
delBase(&b->foo); // Whoaa, we shouldn't delete a member variable.
But how should the compiler distinguish these two cases?
And maybe a bit less contrived:
struct Base {
virtual void hi() { std::cout << "Hello\n";}
};
struct Foo : Base {
void hi() override { std::cout << "Guten Tag\n";}
};
struct Bar : Base {
Foo foo;
};
Bar b;
b.hi() // Hello
b.foo.hi() // Guten Tag
Base *a = &b;
Base *z = &b.foo;
a->hi() // Hello
z->hi() // Guten Tag
But the last two are the same if we have empty base class optimization!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With