In this post we are going to explore how C++ inheritance works.
We will cover things like the memory layout of the objects, vtables, different types of casting and virtual inheritance.
Simple inheritance
Let’s start with the simplest kind of inheritance: only one parent, no virtual functions, no virtual inheritance.
This would be the code.
struct Widget
{
int widget;
};
struct Button
: public Widget
{
int button;
};
And this would be the memory layout of an instance of Button
| bytes | description |
|---|---|
| 4 | widget |
| 4 | button |
As we can see, the memory layout of Button objects is just an aggregation of the member variables.
In this case, inheritance is not very different from composition:
| Button | ||
|---|---|---|
|
||
| button |
static_cast
In this case, static_cast does nothing.
We can test it with the following code.
Button* b = new Button;
cout << b << endl;
Widget* w = static_cast<Widget*>(b);
cout << w << endl;
In my PC the output is:
0x555555767e70
0x555555767e70
That makes sense since, in the table we have seen before, Widget was at the begining of Button.
dynamic_cast
dynamic_cast is for casting downwards.
Widget* w = new Widget;
Button* b = dynamic_cast<Button*>(w);
But in this case it’s not possible because Widget is not polymorphic. So it fails to compile.
error: cannot dynamic_cast ‘w’ (of type ‘struct Widget*’) to type ‘struct Button*’ (source type is not polymorphic)
This is because there are no virtual functions in Widget. We will explore polymorphic classes later.
Multiple inheritance
One class derives from multiple classes:
struct A { int a; };
struct B { int b; };
struct C { int c; };
struct ABC
: public A
, public B
, public C
{
int abc;
};
The memory layout of ABC would come out as:
| bytes | description |
|---|---|
| 4 | a |
| 4 | b |
| 4 | c |
| 4 | abc |
Again, like in the the case of simple inheritance, this works just like composition.
| ABC | ||
|---|---|---|
|
||
|
||
|
||
| abc |
static_cast
For multiple inheritance, static_cast gets a bit more interesting.
Lets run this code:
ABC* abc = new ABC;
cout << "abc: " << abc << endl;
A* a = static_cast<A*>(abc);
cout << "a: " << a << endl;
B* b = static_cast<B*>(abc);
cout << "b: " << b << endl;
C* c = static_cast<C*>(abc);
cout << "c: " << c << endl;
The output is:
abc: 0x56242a01ac20
a: 0x56242a01ac20
b: 0x56242a01ac24
c: 0x56242a01ac28
They are different now. That means that, in order to perform the cast, we need to increment the pointer. The offset is known at compile time.
If you like assembly, have a look at the generated machine code: (compiler explorer)
You will see that, for the first cast, it’s not incrementing anything. However, for the other two casts, it’s adding 4 and 8.