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
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 virtual
ly. 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.