c++multiple-inheritancevirtual-inheritancememory-layout

What is happening under the hood of virtual inheritance?


Recently I have been trying to make a plugin for an old game, and running into a problem similar to Diamond Inheritance.

I have a very reduced example, write as follows:

#include <iostream>
#include <stdint.h>
#include <stddef.h>

using namespace std;

struct CBaseEntity
{
    virtual void Spawn() = 0;
    virtual void Think() = 0;

    int64_t m_ivar{};
};

struct CBaseWeapon : virtual public CBaseEntity
{
    virtual void ItemPostFrame() = 0;

    double m_flvar{};
};

struct Prefab : virtual public CBaseEntity
{
    void Spawn() override { cout << "Prefab::Spawn\n"; }
    void Think() override { cout << "Prefab::Think\n"; }
};

struct WeaponPrefab : virtual public CBaseWeapon, virtual public Prefab
{
    void Spawn() override { cout << boolalpha << m_ivar << '\n'; }
    void ItemPostFrame() override { m_flvar += 1; cout << m_flvar << '\n'; }

    char words[8];
};

int main() noexcept
{
    cout << sizeof(CBaseEntity) << '\n';
    cout << sizeof(CBaseWeapon) << '\n';
    cout << sizeof(Prefab) << '\n';
    cout << sizeof(WeaponPrefab) << '\n';

    cout << offsetof(WeaponPrefab, words) << '\n';
}

The first two are extracted from the game's source code and I made them pure virtual classes since I have no need to instantiate them. The third class (Prefab) is the one I extend all my classes in my mod from.

The problem is: I just noticed that the class size changed, which could potentially indicate an ABI-breaking thingy waiting for me. When I removed all virtual keywords from inheritances, the class size is quite small, and the memory layout make sense to me. But whenever I put virtual inheritance on, the size suddenly blows up, and the layout seems like a mystery.

Like I printed out the offsetof a variable in my WeaponPrefab class, it shows 8, but the total size is 48, which doesn't make any sense - where are the m_ivar and m_flvar?

(I am not trying to provoke people with undefined behavior, but just trying to cope with the existing ABI in the original game.)

Link to Compiler Explorer: https://godbolt.org/z/YvWTbf8j8


Solution

  • Warning: this is all implementation-detail. Different compilers may implement the specifics differently, or may use different mechanisms all together. This is just how GCC does it in this specific situation.

    Note that I'm ignoring the vtable pointers used to implement virtual method dispatch throughout this answer to focus on how virtual inheritance is implemented.

    Using normal, non-virtual inheritance, a WeaponPrefab would include two CBaseEntity sub-objects: one that it inherits via CBaseWeapon and one that it inherits via Prefab. It would look something like this:

     WeaponPrefab
     ┌─────────────────────┐
     │ CBaseWeapon         │
     │ ┌─────────────────┐ │
     │ │ CBaseEntity     │ │
     │ │ ┌─────────────┐ │ │
     │ │ │ int64_t     │ │ │
     │ │ │ ┌─────────┐ │ │ │
     │ │ │ │ m_ivar  │ │ │ │
     │ │ │ └─────────┘ │ │ │
     │ │ └─────────────┘ │ │
     │ │  double         │ │
     │ │  ┌─────────┐    │ │
     │ │  │ m_flvar │    │ │
     │ │  └─────────┘    │ │
     │ └─────────────────┘ │
     │ Prefab              │
     │ ┌─────────────────┐ │
     │ │ CBaseEntity     │ │
     │ │ ┌─────────────┐ │ │
     │ │ │ int64_t     │ │ │
     │ │ │ ┌─────────┐ │ │ │
     │ │ │ │ m_ivar  │ │ │ │
     │ │ │ └─────────┘ │ │ │
     │ │ └─────────────┘ │ │
     │ └─────────────────┘ │
     │  char[8]            │
     │  ┌─────────┐        │
     │  │ words   │        │
     │  └─────────┘        │
     └─────────────────────┘
    

    virtual inheritance allows you to avoid this. Each object will have only one sub-object of each type that it inherits from virtually. In this case, the two CBaseObjects are combined into one:

    WeaponPrefab
    ┌───────────────────┐
    │   char[8]         │
    │   ┌─────────┐     │
    │   │ words   │     │
    │   └─────────┘     │
    │ Prefab            │
    │ ┌───────────────┐ │
    │ └───────────────┘ │
    │ CBaseWeapon       │
    │ ┌───────────────┐ │
    │ │  double       │ │
    │ │  ┌─────────┐  │ │
    │ │  │ m_flvar │  │ │
    │ │  └─────────┘  │ │
    │ └───────────────┘ │
    │ CBaseEntity       │
    │ ┌───────────────┐ │
    │ │  int64_t      │ │
    │ │  ┌─────────┐  │ │
    │ │  │ m_ivar  │  │ │
    │ │  └─────────┘  │ │
    │ └───────────────┘ │
    └───────────────────┘
    

    This presents a problem though. Notice that in the non-virtual example CBaseEntity::m_ivar is always 0-bytes into a Prefab object, whether it's standalone or a sub-object of a WeaponPrefab. But in the virtual example the offset is different. For a standalone Prefab object CBaseEntity::m_ivar would be offset 0-bytes from the start of the object, but for a Prefab that's a sub-object of a WeaponPrefab it would be offset 8-bytes from the start of the Prefab object.

    To get around this problem, objects generally carry an extra pointer to a static table generated by the compiler that contains offsets to each of their virtual base classes:

                                  Offset Table for
    WeaponPrefab                  standalone WeaponPrefab
    ┌────────────────────┐        ┌──────────────────────┐
    │   Offset Table Ptr │        │Prefab offset:      16│
    │   ┌─────────┐      │        │CBaseWeapon offset: 24│
    │   │         ├──────┼───────►│CBaseEntity offset: 40│
    │   └─────────┘      │        └──────────────────────┘
    │   char[8]          │
    │   ┌─────────┐      │
    │   │ words   │      │
    │   └─────────┘      │
    │ Prefab             │        Offset Table for
    │ ┌────────────────┐ │        Prefab in WeaponPrefab
    │ │Offset Table Ptr│ │        ┌──────────────────────┐
    │ │  ┌─────────┐   │ │        │CBaseEntity offset: 24│
    │ │  │         ├───┼─┼───────►│                      │
    │ │  └─────────┘   │ │        └──────────────────────┘
    │ └────────────────┘ │
    │ CBaseWeapon        │        Offset Table for
    │ ┌────────────────┐ │        CBaseWeapon in WeaponPrefab
    │ │Offset Table Ptr│ │        ┌──────────────────────┐
    │ │  ┌─────────┐   │ │        │CBaseEntity offset: 16│
    │ │  │         ├───┼─┼───────►│                      │
    │ │  └─────────┘   │ │        └──────────────────────┘
    │ │  double        │ │
    │ │  ┌─────────┐   │ │
    │ │  │ m_flvar │   │ │
    │ │  └─────────┘   │ │
    │ └────────────────┘ │
    │ CBaseEntity        │
    │ ┌────────────────┐ │
    │ │  int64_t       │ │
    │ │  ┌─────────┐   │ │
    │ │  │ m_ivar  │   │ │
    │ │  └─────────┘   │ │
    │ └────────────────┘ │
    └────────────────────┘
    

    Note that this isn't precisely accurate. Since Prefab has no data members, GCC actually avoids giving it its own offset table and instead has it share WeaponPrefab's table and pointer. This diagram is how GCC would lay the object out if Prefab did have at least one data member.