虚拟继承背后发生了什么?

问题描述 投票:0回答:2

最近在尝试做一个老游戏的插件,遇到了类似钻石传承的问题

我有一个非常简化的例子,写成如下:

#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

c++ multiple-inheritance virtual-inheritance memory-layout
2个回答
4
投票

警告:这都是实现细节。不同的编译器可能以不同的方式实现细节,或者可能一起使用不同的机制。这就是 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如何布局对象。


0
投票

我已经运行了代码,类大小的答案是

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,它可能与您相关,也可能不相关。

© www.soinside.com 2019 - 2024. All rights reserved.