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
virtualmethod dispatch throughout this answer to focus on howvirtualinheritance is implemented.Using normal, non-virtual inheritance, a
WeaponPrefabwould include twoCBaseEntitysub-objects: one that it inherits viaCBaseWeaponand one that it inherits viaPrefab. It would look something like this:virtualinheritance allows you to avoid this. Each object will have only one sub-object of each type that it inherits fromvirtually. In this case, the twoCBaseObjectsare combined into one:This presents a problem though. Notice that in the non-virtual example
CBaseEntity::m_ivaris always 0-bytes into aPrefabobject, whether it's standalone or a sub-object of aWeaponPrefab. But in thevirtualexample the offset is different. For a standalonePrefabobjectCBaseEntity::m_ivarwould be offset 0-bytes from the start of the object, but for aPrefabthat's a sub-object of aWeaponPrefabit would be offset 8-bytes from the start of thePrefabobject.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
virtualbase classes:Note that this isn't precisely accurate. Since
Prefabhas no data members, GCC actually avoids giving it its own offset table and instead has it shareWeaponPrefab's table and pointer. This diagram is how GCC would lay the object out ifPrefabdid have at least one data member.