Understanding the size of virtual derived class

Viewed 190
#include <iostream>
using namespace std;
class A {
  int a;
};
class B1 : virtual public A {
  int b1;
};
class B2 : virtual public A {
  int b2;
};
class C : public B1, public B2 {
  int c;
};

int main() {
  A obj1; B1 obj2; B2 obj3; C obj4;
  cout << sizeof(obj1) << endl;
  cout << sizeof(obj2) << endl;
  cout << sizeof(obj3) << endl;
  cout << sizeof(obj4) << endl;
  return 0;
}

Output:

4
16
16
40

In the above c++ program, size of A is 4 because it has only one int size of B1 is 16 because (int+ int + virtual pointer) same for B2

But how come the size of C is 40???

3 Answers

The result may differ depending on the compiler you are using and the system architecture. For example, msvc 19.28 x64 gives this self-explanatory result (use the option /d1reportAllClassLayout to get it):

class C size(44):
    +---
 0  | +--- (base class B1)
 0  | | {vbptr}
 8  | | b1
    | | <alignment member> (size=4)
    | | <alignment member> (size=4)
    | +---
16  | +--- (base class B2)
16  | | {vbptr}
24  | | b2
    | | <alignment member> (size=4)
    | | <alignment member> (size=4)
    | +---
32  | c
    | <alignment member> (size=4)
    +---
    +--- (virtual base A)
40  | a
    +---

But for msvc 19.28 x86 the result would be:

class C size(24):
    +---
 0  | +--- (base class B1)
 0  | | {vbptr}
 4  | | b1
    | +---
 8  | +--- (base class B2)
 8  | | {vbptr}
12  | | b2
    | +---
16  | c
    +---
    +--- (virtual base A)
20  | a
    +---

Update

It should be noted that the class layout above clearly demonstrates the feature of virtual inheritance, in which only one copy of a base class instance (A) are inherited by grandchild derived class (C). If the A::a data member were public, we could use the following statements inside member function of class C:

void C::foo() {
    B2::a = 1;
    B1::a = 2;
    std::cout << B2::a << " " << B1::a << " " << A::a; // Output: 2 2 2
}

But if you do not use virtual inheritance (but just public), then the class C layout will be like this:

class C size(20):
    +---
 0  | +--- (base class B1)
 0  | | +--- (base class A)
 0  | | | a
    | | +---
 4  | | b1
    | +---
 8  | +--- (base class B2)
 8  | | +--- (base class A)
 8  | | | a
    | | +---
12  | | b2
    | +---
16  | c
    +---

And we would have two copies of member variables of the base class A

void C::foo() {
    B2::a = 1;
    B1::a = 2;
    std::cout << B2::a << " " << B1::a; // Output: 1 2. `A::a` ambiguity error
}

Disclaimer: None of this is specified by the C++ standard, except for the fact that compiler is allowed to add padding, and I haven't actually examined any assembly code.

If compiled in GCC on x64 architecture, the structure might look like this:

+-----------------------------------------------------------------------------------------------------+
| C                                                                                                   |
| +------------------------------------+ +------------------------------------+          +----------+ |
| | B1 (16)                            | | B2 (16)                            |          | A(4)     | |
| | +--------------+ +------+ +------+ | | +--------------+ +------+ +------+ | +------+ | +------+ | |
| | |  vptr (8)    | | b1(4)| |pad(4)| | | |  vptr (8)    | | b2(4)| |pad(4)| | | c(4) | | | a(4) | | |
| | +--------------+ +------+ +------+ | | +--------------+ +------+ +------+ | +------+ | +------+ | |
| +------------------------------------+ +------------------------------------+          +----------+ |
+-----------------------------------------------------------------------------------------------------+

Checking the aligments of each structure (https://wandbox.org/permlink/a4l99p6JwTrSdWx1), we see that A is aligned to 4 bytes, and all the remaining structures are aligned to 8 bytes when using GCC compiler.

I assume that your computer is in x64 architecture. This means pointer must be at least 8 bytes long, and because vptr exist in B1 and B2, the whole structure gets aligned to 8 bytes. Because of this, compiler needs to add padding to both structures, in order to keep sizeof(B1) % alignof(B1) == 0 (or in simple words to keep the structure aligned).

It's not very important to know how exactly C is laid out and padded since this is implementation-defined and will differ between platforms. But there is one specific detail...

Base class A, being virtually inherited, is extracted to the top of the hierarchy. So it's part of B1 only for as long as B1 is used "by itself". When it's inherited further inside C, A is no longer part of it.

That means we can't rely on sizeof(B1) or sizeof(B2), as we have to subtract sizeof(A) from them first. But removing 4 bytes from B1 won't reduce it's size from 16 to 12 since it must be aligned and padded to 8 bytes (assuming 64-bit) due to the vbptr.

So we end up with sizeof(B1 less A)+sizeof(B2 less A)+sizeof(A)+sizeof(C::c) = 16+16+4+4 = 40.

We can get some funky output if we make A larger:

#include <iostream>
using namespace std;
class A {
  int a1,a2,a3,a4,a5;
};
class B1 : virtual public A {
  int b1;
};
class B2 : virtual public A {
  int b2;
};
class C : public B1, public B2 {
  int c;
};

int main() {
  A obj1; B1 obj2; B2 obj3; C obj4;
  cout << "sizeof(A)  = " << sizeof(obj1) << endl;
  cout << "sizeof(B1) = " << sizeof(obj2) << endl;
  cout << "sizeof(B2) = " << sizeof(obj3) << endl;
  cout << "sizeof(C)  = " << sizeof(obj4) << endl;
  return 0;
}

Prints:

sizeof(A)  = 20
sizeof(B1) = 32
sizeof(B2) = 32
sizeof(C)  = 56

Now it's more clear that sizeof(C) < sizeof(B1) + sizeof(B2).

To know more you can play with offsets to see exactly how C is laid out.

Related