Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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).

like image 665
imallett Avatar asked Jan 08 '20 13:01

imallett


1 Answers

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!

like image 196
n314159 Avatar answered Oct 22 '22 06:10

n314159