When Do Programmers Use Empty Base Optimization (Ebo)

When do programmers use Empty Base Optimization (EBO)

EBO is important in the context of policy based design, where you generally inherit privately from multiple policy classes. If we take the example of a thread safety policy, one could imagine the pseudo-code :

class MTSafePolicy
{
public:
void lock() { mutex_.lock(); }
void unlock() { mutex_.unlock(); }

private:
Mutex mutex_;
};

class MTUnsafePolicy
{
public:
void lock() { /* no-op */ }
void unlock() { /* no-op */ }
};

Given a policy based-design class such as :

template<class ThreadSafetyPolicy>
class Test : ThreadSafetyPolicy
{
/* ... */
};

Using the class with a MTUnsafePolicy simply add no size overhead the class Test : it's a perfect example of don't pay for what you don't use.

Why is the empty base class optimization (EBO) is not working in MSVC?

This is a longstanding bug in the Visual C++ compiler. When a class derives from multiple empty base classes, only the initial empty base class will be optimized using the empty base optimization (EBO).

This issue was reported on Microsoft Connect in 2006: Empty Base Optimization Not Working Properly. At the moment, old bugs are not visible on Microsoft Connect. I am told that this is a temporary issue, though I do not know when it will be resolved. In the meantime, the following is the response to the bug from Jonathan Caves, who is one of the developers on the Visual C++ compiler team:

Hi: unfortunately even though this is a bug in the Visual C++ object model we are unable to fix it at this time given that fixing it would potentially break a lot of existing programs as the sizes of objects would change. Hopefully in the future we may be able to address this issue but not for the next release of the product.

Thanks for reporting the issue.

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

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!

Why most implementations of pair don't use compression (empty base optimization) by default?

Implementations of std::pair cannot use empty base optimization (EBO), as the objects it contains are the member subobjects, first and second, and EBO only applies to base class subobjects.

Note that implementations of std::tuple can use EBO, as its subobjects are exposed via std::get, which allows the use of base class subobjects.

Empty Data Member Optimization: would it be possible?

It is coming to c++20 with the [[no_unique_address]] attribute.

The proposal P0840r2 has been accepted into the draft standard. It has this example:

template<typename Key, typename Value, typename Hash, typename Pred, typename Allocator>
class hash_map {
[[no_unique_address]] Hash hasher;
[[no_unique_address]] Pred pred;
[[no_unique_address]] Allocator alloc;
Bucket *buckets;
// ...
public:
// ...
};

empty base class optimization

pe10 == pe11. Can two sub-objects of
two objects have the same address? Is
this conformant?

No, two different objects cannot have same address. If they've, the compiler is not Standard Complaint.

By the way, which version of VC++ you're using? I'm using MSVC++2008, and it's output is this:

alt text

I think, you meant pe20==pe11? If so, then this also is wrong, non-standard. MSVC++2008 compiler has bug!

GCC is correct; see the output yourself : http://www.ideone.com/Cf2Ov


Similar topic:

When do programmers use Empty Base Optimization (EBO)

empty base class optimization

A philosophical argument over the definition of "region" is unnecessary.

1.8/5 says, "Unless it is a bit-field, a most derived object shall have a non-zero size ... Base class sub-objects may have zero size".

So the standard is quite clear what objects (and hence what "regions of storage") can have zero size. If you disagree with the standard what "region" means in English that's one thing, you can fault the authors' (non-programming-related) literary skills. For that matter you can fault their poetic skills (14.7.3/7) But it's quite clear what the standard says here about the sizes of objects of class types.

The pragmatic way to read standards is that given two plausible interpretations of a word, choose the one which doesn't directly contradict another sentence in the same section of the standard. Don't choose the one which matches more closely your personal preferred use of the word, or even the most common use.

Trying to get rid of empty data fields (a kind-of empty base optimization?)

The problem in your solution is that you have 6 independent member variables each of them occupying at least of a single byte, as you are aware of. A better way might be to store the counters into an array, as @scheff suggested in the comments section.

template <size_t N>
class counters {
std::array<size_t, N> values{};

public:
template <size_t I>
void inc() {
static_assert(N > I);
values[I]++;
}

template <size_t I>
size_t get() const {
static_assert(N > I);
return values[I];
}
};

To provide a uniform interface — valid incrementing code in case of disabled counters — a template specialization might be used. In this specialization, there will be no array and empty function bodies:

template<>
class counters<0> {
public:
template <size_t I> void inc() { }
template <size_t I> size_t get() const { return 0; }
};

Finally, the counters might be conditionally injected into your classes be either composition of inheritance. I recommed the latter case, since in case of disabled counters, empty base optimization will be applied:

template<bool enable, size_t N = enable ? 4 : 0>
class potentially_counted : private counters<N> {
int handle; // class' data

enum { BLAH, BLUBB, FOO, BAR };

public:
void blah() { counters<N>::template inc<BLAH>(); }
void blubb() { counters<N>::template inc<BLUBB>(); }
void foo() { counters<N>::template inc<FOO>(); }
void bar() { counters<N>::template inc<BAR>(); }
};

A simple test code:

int main()
{
std::cout << "sizeof(potentially_counted<true>): "
<< sizeof(potentially_counted<true>) << std::endl;
std::cout << "sizeof(potentially_counted<false>): "
<< sizeof(potentially_counted<false>) << std::endl;
}

will likely show that with disabled counting, size of the class will be the same as size of int. Live demo: https://wandbox.org/permlink/sMRrnhZvevozlCBL.


UPDATE

To avoid that non-pleasant templated call, you can wrap it into a separate member function:

template <size_t I>
void inc_cnt() { counters<N>::template inc<I>(); }

And use it as follows:

void blah()  { inc_cnt<BLAH>(); }


Related Topics



Leave a reply



Submit