16

In C++, most of the optimizations are derived from the as-if rule. That is, as long as the program behaves as-if no optimization had taken place, then they are valid.

The Empty Base Optimization is one such trick: in some conditions, if the base class is empty (does not have any non-static data member), then the compiler may elide its memory representation.

Apparently it seems that the standard forbids this optimization on data members, that is even if a data member is empty, it must still take at least one byte worth of place: from n3225, [class]

4 - Complete objects and member subobjects of class type shall have nonzero size.

Note: this leads to the use of private inheritance for Policy Design in order to have EBO kick in when appropriate

I was wondering if, using the as-if rule, one could still be able to perform this optimization.


edit: following a number of answers and comments, and to make it clearer what I am wondering about.

First, let me give an example:

struct Empty {};

struct Foo { Empty e; int i; };

My question is, why is sizeof(Foo) != sizeof(int) ? In particular, unless you specify some packing, chances are due to alignment issues that Foo will be twice the size of int, which seems ridiculously inflated.

Note: my question is not why is sizeof(Foo) != 0, this is not actually required by EBO either

According to C++, it is because no sub-object may have a zero size. However a base is authorized to have a zero size (EBO) therefore:

struct Bar: Empty { int i; };

is likely (thanks to EBO) to obey sizeof(Bar) == sizeof(int).

Steve Jessop seems to be of an opinion that it is so that no two sub-objects would have the same address. I thought about it, however it doesn't actually prevent the optimization in most cases:

If you have "unused" memory, then it is trivial:

struct UnusedPadding { Empty e; Empty f; double d; int i; };
// chances are that the layout will leave some memory after int

But in fact, it's even "worse" than that, because Empty space is never written to (you'd better not if EBO kicks in...) and therefore you could actually place it at an occupied place that is not the address of another object:

struct Virtual { virtual ~Virtual() {} Empty e; Empty f; int i; };
// most compilers will reserve some space for a virtual pointer!

Or, even in our original case:

struct Foo { Empty e; int i; }; // deja vu!

One could have (char*)foo.e == (char*)foo.i + 1 if all we wanted were different address.

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • 1
    Take a look at Boost's [Compressed Pair](http://www.boost.org/doc/libs/1_45_0/libs/utility/compressed_pair.htm) library, to see how to get this optimization. – GManNickG Jan 07 '11 at 09:58
  • @GMan: they cleverly use EBO. But actually this use of EBO is exactly what prompted my question to begin with. – Matthieu M. Jan 07 '11 at 10:07
  • See this : [When do programmers use Empty Base Optimization (EBO)](http://stackoverflow.com/questions/4325144/scenario-when-do-programmers-use-empty-base-optimization-ebo) – Nawaz Jan 07 '11 at 10:33
  • From 9.2.12 "later members have higher addresses within a class object", so your `(char*)foo.e == (char*)foo.i + 1` isn't quite valid ;-P – Tony Delroy Jan 07 '11 at 10:58
  • 1
    "if all we wanted were different address" - I think we don't just want different addresses, though, we want non-overlapping objects. Where "we" is the standard committee. Your virtual case I think is OK, as long as the empty objects are defined to be at the start of a virtual object, or at another position in the class where there are bytes that aren't already part of any subobject. You can also play such tricks where there are access specifiers either side of the empty member, since then it doesn't have to be in order wrt other members. – Steve Jessop Jan 07 '11 at 16:44
  • @Tony: ah thanks for that, then the layout is pretty much fixed (within a visibility group). – Matthieu M. Jan 07 '11 at 17:36

5 Answers5

7

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:
  // ...
};
Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
Ozirus
  • 1,216
  • 13
  • 13
  • @MatthieuM. In the following article, it is said to have been adopted in the draft standard: https://herbsutter.com/2018/04/02/trip-report-winter-iso-c-standards-meeting-jacksonville/ – Ozirus Apr 11 '18 at 19:01
6

Under the as-if rule:

struct A {
    EmptyThing x;
    int y;
};

A a;
assert((void*)&(a.x) != (void*)&(a.y));

The assert must not be triggered. So I don't see any benefit in secretly making x have size 0, when you'd just need to add padding to the structure anyway.

I suppose in theory a compiler could track whether pointers might be taken to the members, and make the optimization only if they definitely aren't. This would have limited use, since there'd be two different versions of the struct with different layouts: one for the optimized case and one for general code.

But for example if you create an instance of A on the stack, and do something with it that is entirely inlined (or otherwise visible to the optimizer), yes, parts of the struct could be completely omitted. This isn't specific to empty objects, though - an empty object is just a special case of an object whose storage isn't accessed, and therefore could in some situations never be allocated at all.

Steve Jessop
  • 273,490
  • 39
  • 460
  • 699
  • yes, this address identity thing is what really annoys me here. While I can guess it could have use, an object whose address is not taken amounts to an object that is not used (since invoking any method on it requires its address) and I don't see how a C++ compiler could realize an object is unused apart from all in-lined classes. Do you know why this non-zero is used for ? (ie if it's a remnant of C or still has used in more modern style) – Matthieu M. Jan 07 '11 at 09:48
  • @Matthieu: well, I suppose that non-zero size of complete objects is so you can do `sizeof(array)/sizeof(*array)` and not divide by zero. I can't think of a strong rationale for non-zero size of member subobjects - if you need some member objects with different addresses, you (the programmer) could just make sure they're of type `char` rather than `EmptyThing`. I guess there's somewhere in the object/memory model that would need special-case handling, and it's just simpler to assume that member subobjects are distinct and non-overlapping. – Steve Jessop Jan 07 '11 at 09:55
  • I've updated my question somewhat. I am not requiring a non-zero size of complete objects (though that too could be nice, allowing heaps of other optimizations, and `sizeof` is compile-time code too, so no worry of a divide by zero exception at runtime anyway). I don't understand why if we can have EBO (that is a zero-size base class) we cannot have EDMO (that is a zero-size data member). – Matthieu M. Jan 07 '11 at 10:47
  • 1
    @Matthieu: if you change the question to, "could the standard be changed to permit EDMO without major knock-on effects", then I think that the answer is "yes, except that it would break existing code assuming that EDMOs have distinct addresses". Your original question was whether a compiler can implement EDMO without explicit permission from the standard, under the as-if rule. To which the answer is still "mostly no". – Steve Jessop Jan 07 '11 at 16:43
  • I agree, unless fully inlined it's impossible to guess if the address of a data member will be used, forbidding this implementation. – Matthieu M. Jan 07 '11 at 17:34
  • 2
    @MatthieuM. "_unless fully inlined it's impossible to guess if the address of a data member will be used_" and you probably wouldn't want the representation of your class to possibly change whenever some member function implementation is changed! – curiousguy Nov 26 '11 at 04:21
2

C++ for technical reasons mandates that empty classes should have non-zero size.
This is to enforce that distinct objects have distinct memory addresses. So compilers silently insert a byte into "empty" objects.
This constraint does not apply to base class parts of derived classes as they are not free-standing.

Cratylus
  • 52,998
  • 69
  • 209
  • 339
1

Because Empty is a POD-type, you can use memcpy to overwrite its "representation", so it better not share it with another C++ object or useful data.

curiousguy
  • 8,038
  • 2
  • 40
  • 58
-1

Given struct Empty { }; consider what happens if sizeof(Empty) == 0. Generic code that allocates heap for Empty objects could easily behave differently, as - for example - a realloc(p, n * sizeof(T)), where T is Empty, is then equivalent to free(p). If sizeof(Empty) != 0 then things like memset/memcpy etc. would try to work on memory regions that weren't in use by the Empty objects. So, the compiler would need to stitch up things like sizeof(Empty) on the basis of the eventual usage of the value - that sounds close to impossible to me.

Separately, under current C++ rules the assurance that each member has a distinct address means you can use those addresses to encode some state about those fields - e.g. a textual field name, whether some member function of the field object should be visited etc.. If addresses suddenly coincide, any existing code reliant on these keys could break.

Tony Delroy
  • 102,968
  • 15
  • 177
  • 252
  • "things like memset/memcpy etc. would try to work on memory regions that weren't in use by the Empty objects" - only if you passed in a wrong (i.e non-0) length to memset/memcpy. – Steve Jessop Jan 07 '11 at 09:59
  • @Tony: I was not suggesting that `sizeof(Empty)` be `0`, but rather that given `struct Foo { Empty e; int i; };` one had `sizeof(Foo) == sizeof(int)` which would mean saving about 4/8 bytes of memory here (because of alignment) – Matthieu M. Jan 07 '11 at 10:05
  • @Steve: exactly :-) - that sentence starts with "sizeof(Empty) != 0", meaning that when you calculate the size for the memset/memcpy call using `sizeof(T)` where T is Empty, you'd get a non-zero value... – Tony Delroy Jan 07 '11 at 10:15
  • @Matthieu: so `sizeof(Empty) != sizeof Foo.e`, `sizeof(Foo) < sizeof Foo.e + sizeof Foo.i`...? :-) All I'm saying is I think there are too many corner-cases just with templated code using `sizeof` (perhaps most involve C-style pointer hackery, memset, malloc/realloc etc rather than the C++-only equivalents - but they must continue to work as expected). – Tony Delroy Jan 07 '11 at 10:18
  • @Tony: `sizeof(Foo) != sizeof(Foo.e) + sizeof(Foo.i)` and `sizeof(Foo.e)` is independent from the memory representation of `e` in `Foo`, after all it does not indicate the padding that follows `e` for example. Also, in the case of `EBO`, you already have `sizeof(Bar) < sizeof(Empty) + sizeof(Bar.i)`. I am unsure of the pointer hackery argument, I am not too concern by alloc (you don't use malloc/realloc on subjobjects) and the `memset` / `memcpy` use is dangerous: what if it's changed to a base class instead of a data member ? Then EBO may kick in... – Matthieu M. Jan 07 '11 at 10:43
  • @Matthieu: good point re the similarities with EBO... that it rarely causes problems may mean there would be few practical issues, but am trying to explore possible ones.... – Tony Delroy Jan 07 '11 at 11:09
  • @Tony: and I thank you for exploring :) Which is also why I am taking time of my own to try and address the several point you raise. I must admit I'd much prefer to be able to have 0-size objects as this would allows some memory savings, maybe I'll try suggesting it into clang (with a specific optimization flag), but they might balk at it because they usually don't appreciate non-standard code. – Matthieu M. Jan 07 '11 at 13:19