最近在尝试做一个老游戏的插件,遇到了类似钻石传承的问题
我有一个非常简化的例子,写成如下:
#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';
}
前两个是从游戏源代码中提取出来的,我把它们做成纯虚拟类,因为我不需要实例化它们。 第三堂课 (
Prefab
) 是我在我的 mod 中扩展所有课程的那一堂课。
问题是: 我刚刚注意到班级人数发生了变化,这可能表明有一个破坏 ABI 的东西在等着我。当我从继承中删除所有
virtual
关键字时,类的大小非常小,内存布局对我来说很有意义。但是每当我把virtual
继承放在上面的时候,尺寸突然就膨胀了,而且布局看起来像个谜
就像我在我的
offsetof
类中打印出WeaponPrefab
一个变量,它显示8
,但总大小是48
,这没有任何意义-m_ivar
和m_flvar
在哪里
?
(我不是想挑起未定义行为的人,只是想应付原游戏中已有的ABI。)
编译器资源管理器链接:https://godbolt.org/z/YvWTbf8j8
警告:这都是实现细节。不同的编译器可能以不同的方式实现细节,或者可能一起使用不同的机制。这就是 GCC 在这种特定情况下的做法。
请注意,我在整个答案中忽略了用于实现
virtual
方法调度的 vtable 指针,以专注于如何实现 virtual
继承。
使用正常的非虚拟继承,
WeaponPrefab
将包含两个 CBaseEntity
子对象:一个通过 CBaseWeapon
继承,一个通过 Prefab
继承。它看起来像这样:
WeaponPrefab
┌─────────────────────┐
│ CBaseWeapon │
│ ┌─────────────────┐ │
│ │ CBaseEntity │ │
│ │ ┌─────────────┐ │ │
│ │ │ int64_t │ │ │
│ │ │ ┌─────────┐ │ │ │
│ │ │ │ m_ivar │ │ │ │
│ │ │ └─────────┘ │ │ │
│ │ └─────────────┘ │ │
│ │ double │ │
│ │ ┌─────────┐ │ │
│ │ │ m_flvar │ │ │
│ │ └─────────┘ │ │
│ └─────────────────┘ │
│ Prefab │
│ ┌─────────────────┐ │
│ │ CBaseEntity │ │
│ │ ┌─────────────┐ │ │
│ │ │ int64_t │ │ │
│ │ │ ┌─────────┐ │ │ │
│ │ │ │ m_ivar │ │ │ │
│ │ │ └─────────┘ │ │ │
│ │ └─────────────┘ │ │
│ └─────────────────┘ │
│ char[8] │
│ ┌─────────┐ │
│ │ words │ │
│ └─────────┘ │
└─────────────────────┘
virtual
继承可以让你避免这种情况。每个对象将只有一个从 virtual
ly 继承的每种类型的子对象。在这种情况下,两个CBaseObjects
合并为一个:
WeaponPrefab
┌───────────────────┐
│ char[8] │
│ ┌─────────┐ │
│ │ words │ │
│ └─────────┘ │
│ Prefab │
│ ┌───────────────┐ │
│ └───────────────┘ │
│ CBaseWeapon │
│ ┌───────────────┐ │
│ │ double │ │
│ │ ┌─────────┐ │ │
│ │ │ m_flvar │ │ │
│ │ └─────────┘ │ │
│ └───────────────┘ │
│ CBaseEntity │
│ ┌───────────────┐ │
│ │ int64_t │ │
│ │ ┌─────────┐ │ │
│ │ │ m_ivar │ │ │
│ │ └─────────┘ │ │
│ └───────────────┘ │
└───────────────────┘
但这提出了一个问题。请注意,在非虚拟示例中,
CBaseEntity::m_ivar
始终是 0 字节到 Prefab
对象中,无论它是独立对象还是 WeaponPrefab
的子对象。但在 virtual
示例中,偏移量不同。对于独立的 Prefab
对象,CBaseEntity::m_ivar
将从对象的开头偏移 0 字节,但对于作为 Prefab
的子对象的 WeaponPrefab
,它将从对象的开头偏移 8 字节Prefab
对象。
为了解决这个问题,对象通常携带一个额外的指针指向它们的所有
virtual
祖先,以便它们可以定位它们的成员,无论它们是独立存在的还是作为进一步派生类的子对象存在:
WeaponPrefab
┌───────────────────┐
│ CBaseWeapon* │
│ ┌─────────┐ │
│ │ ├─────┼─────┐
│ └─────────┘ │ │
│ Prefab* │ │
│ ┌─────────┐ │ │
│ │ ├─────┼─┐ │
│ └─────────┘ │ │ │
│ char[8] │ │ │
│ ┌─────────┐ │ │ │
│ │ words │ │ │ │
│ └─────────┘ │ │ │
│ Prefab │ │ │
│ ┌───────────────┐ │ │ │
│ │ CBaseEntity* │◄├─┘ │
│ │ ┌─────────┐ │ │ │
│ │ │ ├──┼─┼───┐ │
│ │ └─────────┘ │ │ │ │
│ └───────────────┘ │ │ │
│ CBaseWeapon │ │ │
│ ┌───────────────┐ │ │ │
│ │ CBaseEntity* │◄│───│─┘
│ │ ┌─────────┐ │ │ │
│ │ │ ├──┼─┼─┐ │
│ │ └─────────┘ │ │ │ │
│ │ double │ │ │ │
│ │ ┌─────────┐ │ │ │ │
│ │ │ m_flvar │ │ │ │ │
│ │ └─────────┘ │ │ │ │
│ └───────────────┘ │ │ │
│ CBaseEntity │ │ │
│ ┌───────────────┐ │ │ │
│ │ int64_t │◄├─┘ │
│ │ ┌─────────┐ │ │ │
│ │ │ m_ivar │ │◄├───┘
│ │ └─────────┘ │ │
│ └───────────────┘ │
└───────────────────┘
请注意,这并不准确。因为
Prefab
没有数据成员,所以 GCC
实际上做了一些不同的事情来避免有一个额外的 Prefab
指针。如果Prefab
did至少有一个数据成员,下图是GCC如何布局对象。
我已经运行了代码,类大小的答案是
sizeof(CBaseEntity) = 16
sizeof(CBaseWeapon) = 32
sizeof(Prefab) = 24
sizeof(WeaponPrefab) = 48
一般来说,虚函数和虚继承的实现是实现定义的,并且可能因编译器和其他选项而异。话虽这么说,也许我可以对对象的大小提供一些解释,至少对于可能的实现是这样。
CBaseEntity
只是一个多态类型,因此有一个指向 vtable
的指针(对于我所知道的所有 C++ 实现都是如此,但不是标准强制要求的),它还包含 int64
。指针的大小 = 8,int64
的大小 = 8,所以总共正好是 16.
CBaseWeapon
继承自 CBaseEntity
并持有双倍。它的大小必须至少为 24。现在虚拟继承意味着 CBaseWeapon
和 CBaseEntity
的对象位置之间的差异不固定 - 只有最终类决定它。此信息需要存储在类的实例中。我相信该信息位于CBaseWeapon
布局开头的某个位置。为了包含此信息,应该添加填充,以便根据对齐要求大小可以被 8 整除。因此,总大小总和为 32。基本上,它在 CBaseEntity
之上增加了 16
Prefab
类似于 CBaseWeapon
,但它不包含 double
。所以 24 或 8 在 CBaseEntity
.
WeaponPrefab
几乎继承了CBaseEntity
、CBaseWeapon
、Prefab
,并包含了char[8]
。所以,它已经需要16+16+8+8 = 48
。如果有的话,令人惊讶的是 WeaponPrefab
没有变大。这可能是因为 Prefab
不存储任何对象,并且这两个类以某种方式共享优化类存储大小的布局位置变量。比方说,如果你给 Prefab
添加一个 double 成员,那么 WeaponPrefab
的大小将增加到 64.
但是,正如我之前所说,这在很大程度上取决于确切的规格。我不知道您为哪个平台编写代码。我确信 ABI 的规范在互联网上的某个地方,您可以查找详细信息。例如,查看 Itanium C++ ABI,它可能与您相关,也可能不相关。