Inheritance and virtual functions

Contents

  • Object layout
  • Inheritance
  • Multiple inheritance
  • Virtual functions
  • Virtual inheritance

Object Layout

class Pawn {
    int m_Id;
    bool m_IsAlive;
    int m_HP;
}

The members of the class are laid out in the same order in memory as they are declared.

Dumping the layout of a struct.

clang++ -cc1 -fdump-record-layouts file.cxx

http://eli.thegreenplace.net/2012/12/17/dumping-a-c-objects-memory-layout-with-clang

0 | struct Pawn
0 |   int m_Id
4 |   _Bool m_IsAlive
8 |   int m_HP
  | [sizeof=12, dsize=12, align=4
  |  nvsize=12, nvalign=4]
  • The digit in the first column is the offset of the member in the stuct.
  • sizeof is how much the sizeof operator will return
  • dsize is the size without the extra tail padding
  • align is the alignment of the structure

Alignment

https://en.wikipedia.org/wiki/Data_structure_alignment

In short - the CPU is most effective to load data at addresses that are multiple of a 4, 8, 16, 64, etc. Therefore the compiler generates the code so that the address of every variable is a multiple of its alignment.

In C++11, the alignof operator gives the alignment of a type.

static_assert(alignof(bool) == 1, "the alignment of bool is 1");
  • m_IsAlive is bool, size 1, alignment 1
  • m_HP is int - size 4, alignment 4

The compiler must create a hole of 3 bytes in Pawn in order to fulfill the alignment of m_HP.

Padding

The extra empty spaces that the compiler places inside user-defined types in order to properly align their members.

Padding caveats

The padding is filled with random data. You SHOULD never use it.

Pawn p1, p2;

if (std::memcmp(&p1, &p2, sizeof(p1)))

This will take the 3 random bytes in p1 and p2 into the comparison and might return that they are different, although all of their data members are actually equal.

Inheritance

struct Player : Pawn {
    const char* m_Name;
    int m_Score;
};

The alignment of pointers depends on the architecture. So the layout also depends on the architecture.

64-bit architecture

 0 | struct Player
 0 |   struct Pawn (base)
 0 |     int m_Id
 4 |     _Bool m_IsAlive
 8 |     int m_HP
16 |   const char * m_Name
24 |   int m_Score
   | [sizeof=32, dsize=28, align=8
   |  nvsize=28, nvalign=8]

32-bit architecture

 0 | struct Player
 0 |   struct Pawn (base)
 0 |     int m_Id
 4 |     _Bool m_IsAlive
 8 |     int m_HP
12 |   const char * m_Name
16 |   int m_Score
   | [sizeof=20, dsize=20, align=4
   |  nvsize=20, nvalign=4]

Multiple inheritance

struct Enemy {
    int m_Damage;
};

struct EnemyPawn : Pawn, Enemy {
    int m_TargetId;
};
0 | struct Enemy
0 |   int m_Damage
  | [sizeof=4, dsize=4, align=4
  |  nvsize=4, nvalign=4]
 0 | struct EnemyPawn
 0 |   struct Pawn (base)
 0 |     int m_Id
 4 |     _Bool m_IsAlive
 8 |     int m_HP
12 |   struct Enemy (base)
12 |     int m_Damage
16 |   int m_TargetId
   | [sizeof=20, dsize=20, align=4
   |  nvsize=20, nvalign=4]

The members of the derived class are laid out after the members of the base class.

Virtual Functions

struct Pawn {
    virtual ~Pawn() {} 
    virtual void NotOverridden() {}
    virtual int Move() { return 0; }
    int m_Id;
    bool m_IsAlive;
    int m_HP;
};
struct Player : Pawn {
    virtual ~Player() {}
    virtual int Move() override { return 42; }
    const char* m_Name;
    int m_Score;
};

Virtual functions in C++ are implemented with the so called virtual table. The virtual table is an array of pointers to methods. Whenever a virtual method is called, the code lookups the pointer to the method in the virtual table and executes it.

Pawn* p = new Player;

p->Move();
// actually it is:
(p->*(p->vtable[index_for_Move]))();
Vtable for Pawn
Pawn::vtable for Pawn: 6u entries
0     (int (*)(...))0
8     (int (*)(...))(& typeinfo for Pawn)
16    (int (*)(...))Pawn::~Pawn
24    (int (*)(...))Pawn::~Pawn
32    (int (*)(...))Pawn::NotOverridden
40    (int (*)(...))Pawn::Move
Player::vtable for Player: 6u entries
0     (int (*)(...))0
8     (int (*)(...))(& typeinfo for Player)
16    (int (*)(...))Player::~Player
24    (int (*)(...))Player::~Player
32    (int (*)(...))Pawn::NotOverridden
40    (int (*)(...))Player::Move

Layout

 0 | struct Pawn
 0 |   (Pawn vtable pointer)
 8 |   int m_Id
12 |   _Bool m_IsAlive
16 |   int m_HP
   | [sizeof=24, dsize=20, align=8
   |  nvsize=20, nvalign=8]
 0 | struct Player
 0 |   struct Pawn (primary base)
 0 |     (Pawn vtable pointer)
 8 |     int m_Id
12 |     _Bool m_IsAlive
16 |     int m_HP
24 |   const char * m_Name
32 |   int m_Score
   | [sizeof=40, dsize=36, align=8
   |  nvsize=36, nvalign=8]

clang++'s -fdump-vtable-laouts did not output anything.

The virtual tables are dumped with:

g++ -fdump-class-hierarchy -std=c++11 -c vlayout.cc 

Multiple-inheritance

class FlyingPawn : public Pawn, public Flying
{
    int m_FlyingPawnMember;
};

The same as single inheritance, all the base classes are at the beginning of the object in the order their are inherited.

Given:

class Flying {
    public:
        int altitude;
};

Pointer assignment

FlyingPawn fp;
Flying* f = &fp;
cout << f->altitude << std::endl;
// compiler generates cout << reinterpret_cast_cast<int*>(f) << endl;

What is requried from Flying* f = &fp; in order for the code to work?

FlyingPawn fp;
Flying* f = &fp;
// compiler generates f = &fp + offset_of_Flying_in_FlyingPawn

The offset is known at compile time, so there is no significant runtime penalty.

Flying* f = new FlyingPawn;
delete f;

Undefined behaviour. Best case - warning for deleting an object with virtual method and without virtual destructor.

Virtual inheritance

The idea is simple - instead of having the base class at a fixed at compile time offset, use a runtime offset.

Runtime offset - actually a pointer to the base class.

struct Player : virtual Pawn {
    const char* m_Name;
    int m_Score;
};

The compiler generates a pointer to Pawn at the beginning of Player and places the actual Pawn instance somewhere in Player.

Somewhere is at the end for most implementations.

Multiple-inheritance with virtual functions

The object gets multiple virtual tables, one for each base class with virtual functions. Each virtual table has the same indices as the virtual table of the corresponding base class.

Setting of the vtable pointer

Pseudo-C++ class for virtual tables

class VTable;
struct Pawn {
    Pawn(int health)
        : _vtbl(VTable_Pawn) // added by the compiler
        , m_Health(healt)
    {}
    virtual ~Pawn()
        : _vtbl(VTable_Pawn) // added by the compiler
    {}
    VTable* _vtbl;
    int m_Health;
};
struct Player : Pawn {
    Player(int health, int score)
        : Pawn(health)
        , _vtbl(VTable_Player)
        , m_Score(score)
    {}
    virtual ~Player()
        : _vtbl(VTable_Pawn) // added by the compiler
    {
        this->~Pawn(); // added by the compiler
    }
    VTable* _vtbl;
    int m_Score;
};

virtual functions are not virtual inside constructors and destructors

Otherwise the virtual function will execute over not-yet constructed or already destroyed object.

C++11 final classes

Cannot be derived from.

  • show intention
  • optimizations - de-virtualization