9

I know the memory layout of multiple inheritance is not defined, so I should not rely on it. However, can I rely on it in a special case. That is, a class has only one "real" super class. All others are "empty classes", i.e., classes that neither have fields nor virtual methods (i.e. they only have non-virtual methods). In this case, these additional classes should not add anything to the memory layout of the class. (More concisely, in the C++11 wording, the class has standard-layout)

Can I infer that all the superclasses will have no offset? E.g.:

#include <iostream>

class X{

    int a;
    int b;
};

class I{};

class J{};

class Y : public I, public X,  public J{};

int main(){

    Y* y = new Y();
    X* x = y;
    I* i = y;
    J* j = y;

    std::cout << sizeof(Y) << std::endl 
                  << y << std::endl 
                  << x << std::endl 
                  << i << std::endl 
                  << j << std::endl;
}

Here, Y is the class with X being the only real base class. The output of the program (when compiled on linux with g++4.6) is as follows:

8

0x233f010

0x233f010

0x233f010

0x233f010

As I concluded, there is no pointer adjustment. But is this implementation specific or can I rely on it. I.e., if I receive an object of type I (and I know only these classes exist), can I use a reinterpret_cast to cast it to X?

My hopes are that that I could rely on it because the spec says that the size of an object must at least be a byte. Therefore, the compiler cannot choose another layout. If it would layout I and J behind the members of X, then their size would be zero (because they have no members). Therefore, the only reasonable choice is to align all super classes without offset.

Am I correct or am I playing with the fire if I use reinterpret_cast from I to X here?

4
  • 3
    Memory layout of inheritance, single or multiple, empty bases or not, is not defined. Commented Jun 15, 2012 at 9:34
  • 1
    I'd say you're playing with fire, because you're trying to make a very strange construct which will certainly cause pain and misery to anyone who has to understand it in future, even if it doesn't break when you upgrade your compiler! Commented Jun 15, 2012 at 9:35
  • luckily, your statements are no longer correct in C++11, see the answer :) Commented Jun 15, 2012 at 11:29
  • @n.m. it is implementation defined. The implementation is required to document it, so it is defined, just not in the standard. Commented Jun 15, 2012 at 11:47

1 Answer 1

11

In C++11 the compiler is required to use the Empty Base-class Optimization for standard layout types. see https://stackoverflow.com/a/10789707/981959

For your specific example all the types are standard layout classes and don't have common base classes or members (see below) so you can rely on that behaviour in C++11 (and in practice, I think many compilers already followed that rule, certainly G++ did, and others following the Itanium C++ ABI.)

A caveat: make sure you don't have any base classes of the same type, because they must be at distinct addresses, e.g.

struct I {};

struct J : I {};
struct K : I { };

struct X { int i; };

struct Y : J, K, X { };

#include <iostream>

Y y;

int main()
{
  std::cout << &y << ' ' << &y.i << ' ' << (X*)&y << ' ' << (I*)(J*)&y << ' ' << (I*)(K*)&y << '\n';

}

prints:

0x600d60 0x600d60 0x600d60 0x600d60 0x600d61

For the type Y only one of the I bases can be at offset zero, so although the X sub-object is at offset zero (i.e. offsetof(Y, i) is zero) and one of the I bases is at the same address, but the other I base is (at least with G++ and Clang++) one byte into the object, so if you got an I* you couldn't reinterpret_cast to X* because you wouldn't know which I sub-object it pointed to, the I at offset 0 or the I at offset 1.

It's OK for the compiler to put the second I sub-object at offset 1 (i.e. inside the int) because I has no non-static data members, so you can't actually dereference or access anything at that address, only get a pointer to the object at that address. If you added non-static data members to I then Y would no longer be standard layout and would not have to use the EBO, and offsetof(Y, i) would no longer be zero.

Sign up to request clarification or add additional context in comments.

5 Comments

yes, of course. I must ensure that the class has standard-layout. Thanks!
(Updated the example to be closer to your original). In my example Y is standard layout, but it wouldn't be safe to reinterpret_cast an I* to J*, K*, Y* or X* because you wouldn't know which I* it points to.
okay, so no inhertance diamonds and only standard-layout. Right like that?
At the time you wrote this answer all the types in your example were standard-layout types, but they are not anymore because of [class.prop](3.5). However, if you run your code in the most recent versions of GCC and clang, basically you still get the same addresses. Could you explain how this is possible, given that pointers to Y and J and pointers to Y and K are no longer pointer-intercovertible?
@Belloc the rules of the Itanium ABI are not specified in terms of standard-layout or pointer-interconvertibility, so the required layout does not change when the definition of standard-layout changes. Everything in the last two paragraphs ("For the type Y..." and "It's OK for the compiler to put ...") is still true in C++20. Replace "standard layout" with "POD for the purposes of layout" if you prefer.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.