Why is empty base optimization forbidden when the empty base class is also a member variable?
Asked Answered
M

1

14

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 StandardLayoutTypes (though Bar, above, is not a StandardLayoutType).

Maulmain answered 8/1, 2020 at 13:55 Comment(5)
How does the rationale you quoted ("since the two base subobjects of the same type are required to have different addresses") fall short? Different objects of the same type are required to have distinct addresses, and this requirement ensures we don't break that rule. If empty base optimisation applied here, we could have Base *a = new Bar(); Base *b = a->foo; with a==b, but a and b are clearly different objects (perhaps with different virtual method overrides).Mirna
The language-lawyer answer is quoting the relevant parts of the spec. And it seems you already know about that.Returnable
I'm not sure I understand what kind of answer you're looking for here. The C++ object model is what it is. The restriction is there because the object model requires it. What more are you looking for beyond that?Jimjimdandy
@TobySpeight Different objects of the same type are required to have distinct addresses It is easily possible to break this rule in a program with well-defined behavior.Josphinejoss
@TobySpeight No, I don't mean that you forgot to say about lifetime: "Different object of the same type withing their lifetime". It is possible to have multiple objects of the same type, all alive, at the same address. There are at least 2 bugs in the wording allowing this.Josphinejoss
K
4

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!

Keddah answered 8/1, 2020 at 14:14 Comment(13)
One could argue that the second call has undefined behavior anyway, though. So the compiler doesn't have to distinguish anything.Ovariotomy
@Returnable - From the post with added virtual destructorsOvariotomy
class/struct with virtual methods - already not empty (will be containing vtable). so all this already unrelated to questionRights
Yes, the deleting is not a good example. I added something better.Keddah
A class with any virtual members is not empty, so irrelevant here!Returnable
@Returnable Do you have a standard quote on that? Cppref tells us that an empty class is "a class or struct that has no non-static data members".Keddah
@Keddah std::is_empty on cppreference is far more elaborate. Same from the current draft on eel.is.Returnable
@Returnable Yes, that seems indeed a better source. I didn't find anything in the standard regarding empty base classes. I added another example that should hopefully work without any virtual methods.Keddah
@Returnable only in C++20 does std::is_empty indicate exactly those types for which EBO is valid. All C++17 says is "Base class subobjects may have zero size."intro.objectThacher
You can't dynamic_cast when it's not polymorphic (with minor exceptions not relevant here).Equinoctial
While I appreciate the effort you put into this answer, all three of your current examples rely on the object having a vtable (≈ being polymorphic), which makes the object not a StandardLayoutType, and therefore EBO not required. So, this isn't really a complete explanation.Maulmain
@imallett Yes, it seems that I was just wrong. I updated the answer.Keddah
(Accepted answer.) Well, it does seem that there is no current good reason for the object model to be this way, but that it apparently is. Good info about C++20, too.Maulmain

© 2022 - 2024 — McMap. All rights reserved.