Понимание размера виртуального производного класса

#include <iostream>
using namespace std;
class A {
  int a;
};
class B1 : virtual public A {
  int b1;
};
class B2 : virtual public A {
  int b2;
};
class C : public B1, public B2 {
  int c;
};

int main() {
  A obj1; B1 obj2; B2 obj3; C obj4;
  cout << sizeof(obj1) << endl;
  cout << sizeof(obj2) << endl;
  cout << sizeof(obj3) << endl;
  cout << sizeof(obj4) << endl;
  return 0;
}

Выход:

4
16
16
40

В приведенной выше программе на С++ размер A равен 4, потому что он имеет только один int, размер B1 равен 16, потому что (int + int + виртуальный указатель) то же самое для B2

Но почему размер C равен 40???


person the_razz03    schedule 08.01.2021    source источник
comment
Может быть выравнивание и дополнение   -  person drescherjm    schedule 08.01.2021
comment
@drescherjm Все int. Что там выравнивать или прокладывать?   -  person Spencer    schedule 08.01.2021
comment
@Spencer Вероятно, это архитектура x64, поэтому vptr составляет 8 байт.   -  person Yksisarvinen    schedule 08.01.2021
comment
Я ожидаю, что вы не увидите этого, если int будет того же размера, что и ptr.   -  person drescherjm    schedule 08.01.2021
comment
@Yksisarvinen Тогда размер будет 44, нет?   -  person Spencer    schedule 08.01.2021
comment
@Yksisarvinen Я думаю, у вас есть правильное представление о добавленном vptr, предполагающем 32-битную архитектуру. Связанный вопрос не касается vptr в виртуальном наследовании, поэтому, возможно, вам следует опубликовать ответ здесь.   -  person Spencer    schedule 08.01.2021
comment
FWIW, нет причин действительно понимать это, если вы не заинтересованы в написании собственного компилятора. Механика работы полиморфизма в C++ зависит от реализации, поэтому каждый поставщик компилятора может сделать это по-своему.   -  person NathanOliver    schedule 08.01.2021
comment
16+16+вптр выглядит=40?   -  person Ayxan Haqverdili    schedule 08.01.2021
comment
@AyxanHaqverdili 16+16+4+vptr. Помните, что у A также есть int.   -  person Spencer    schedule 08.01.2021
comment
@Spencer vptr, вероятно, составляет 8 байтов.   -  person Ayxan Haqverdili    schedule 08.01.2021
comment
@AyxanHaqverdili См. комментарий выше.   -  person Spencer    schedule 08.01.2021
comment
Но почему размер C равен 40??? -- Это действительно забавно, когда программисты не могут поверить, что компилятор действительно может быть прав, когда он возвращает sizeof(), который не соответствует их ожиданиям. Мол, эй, ты не должен давать мне такой ответ.   -  person PaulMcKenzie    schedule 08.01.2021
comment
@PaulMcKenzie Это распространенная ошибка новичка.   -  person Spencer    schedule 08.01.2021
comment
40 = 16 + 16 + sizeof(int) + заполнение до числа, кратного 8, для требования выравнивания указателей.   -  person molbdnilo    schedule 08.01.2021
comment
Радж: Дайте нам знать, компилируете ли вы с 32- или 64-битной архитектурой, а также о том, что вы получаете, когда компилируете с другой архитектурой.   -  person Spencer    schedule 08.01.2021
comment
Вот исправленный код, показывающий размер указателя: https://ideone.com/JFy4Om   -  person drescherjm    schedule 08.01.2021
comment
Пожалуйста, опишите вашу арку, особенно напечатайте sizeof(int) и sizeof(void*)   -  person curiousguy    schedule 08.01.2021


Ответы (3)


Результат может отличаться в зависимости от используемого компилятора и архитектуры системы. Например, msvc 19.28 x64 дает следующий очевидный результат (используйте опцию /d1reportAllClassLayout, чтобы получить его):

class C size(44):
    +---
 0  | +--- (base class B1)
 0  | | {vbptr}
 8  | | b1
    | | <alignment member> (size=4)
    | | <alignment member> (size=4)
    | +---
16  | +--- (base class B2)
16  | | {vbptr}
24  | | b2
    | | <alignment member> (size=4)
    | | <alignment member> (size=4)
    | +---
32  | c
    | <alignment member> (size=4)
    +---
    +--- (virtual base A)
40  | a
    +---

Но для msvc 19.28 x86 результатом будет:

class C size(24):
    +---
 0  | +--- (base class B1)
 0  | | {vbptr}
 4  | | b1
    | +---
 8  | +--- (base class B2)
 8  | | {vbptr}
12  | | b2
    | +---
16  | c
    +---
    +--- (virtual base A)
20  | a
    +---

Обновлять

Следует отметить, что приведенная выше структура классов ясно демонстрирует функцию виртуального наследования, при котором только одна копия экземпляра базового класса (A) наследуется производным классом-внуком (C). Если бы элемент данных A::a был общедоступным, мы могли бы использовать следующие операторы внутри функции-члена класса C:

void C::foo() {
    B2::a = 1;
    B1::a = 2;
    std::cout << B2::a << " " << B1::a << " " << A::a; // Output: 2 2 2
}

Но если не использовать виртуальное наследование (а просто публичное), то макет класса C будет таким:

class C size(20):
    +---
 0  | +--- (base class B1)
 0  | | +--- (base class A)
 0  | | | a
    | | +---
 4  | | b1
    | +---
 8  | +--- (base class B2)
 8  | | +--- (base class A)
 8  | | | a
    | | +---
12  | | b2
    | +---
16  | c
    +---

И у нас было бы две копии переменных-членов базового класса A

void C::foo() {
    B2::a = 1;
    B1::a = 2;
    std::cout << B2::a << " " << B1::a; // Output: 1 2. `A::a` ambiguity error
}
person alex_noname    schedule 08.01.2021

Отказ от ответственности: ничего из этого не указано в стандарте C++, за исключением того факта, что компилятору разрешено добавлять отступы, и я фактически не исследовал какой-либо ассемблерный код.

Если скомпилировать в GCC на архитектуре x64, структура может выглядеть так:

+-----------------------------------------------------------------------------------------------------+
| C                                                                                                   |
| +------------------------------------+ +------------------------------------+          +----------+ |
| | B1 (16)                            | | B2 (16)                            |          | A(4)     | |
| | +--------------+ +------+ +------+ | | +--------------+ +------+ +------+ | +------+ | +------+ | |
| | |  vptr (8)    | | b1(4)| |pad(4)| | | |  vptr (8)    | | b2(4)| |pad(4)| | | c(4) | | | a(4) | | |
| | +--------------+ +------+ +------+ | | +--------------+ +------+ +------+ | +------+ | +------+ | |
| +------------------------------------+ +------------------------------------+          +----------+ |
+-----------------------------------------------------------------------------------------------------+

Проверяя выравнивание каждой структуры (https://wandbox.org/permlink/a4l99p6JwTrSdWx1), мы видим что A выравнивается по 4 байтам, а все остальные структуры выравниваются по 8 байтам при использовании компилятора GCC.

Я предполагаю, что ваш компьютер имеет архитектуру x64. Это означает, что длина указателя должна быть не менее 8 байт, а поскольку vptr существует в B1 и B2, вся структура выравнивается по 8 байтам. Из-за этого компилятору необходимо добавить отступы к обеим структурам, чтобы сохранить sizeof(B1) % alignof(B1) == 0 (или, проще говоря, чтобы структура оставалась выровненной).

person Yksisarvinen    schedule 08.01.2021

Не очень важно знать, как именно C размещается и дополняется, поскольку это определяется реализацией и будет различаться на разных платформах. Но есть одна специфическая деталь...

Базовый класс A, будучи фактически унаследованным, извлекается на вершину иерархии. Так что это часть B1 только до тех пор, пока B1 используется сам по себе. Когда он наследуется внутри C, A больше не является его частью.

Это означает, что мы не можем полагаться на sizeof(B1) или sizeof(B2), так как сначала нужно вычесть из них sizeof(A). Но удаление 4 байтов из B1 не уменьшит его размер с 16 до 12, поскольку он должен быть выровнен и дополнен до 8 байтов (при условии, что он 64-битный) из-за vbptr.

Таким образом, мы получаем sizeof(B1 less A)+sizeof(B2 less A)+sizeof(A)+sizeof(C::c) = 16+16+4+4 = 40.

Мы можем получить необычный результат, если увеличим A:

#include <iostream>
using namespace std;
class A {
  int a1,a2,a3,a4,a5;
};
class B1 : virtual public A {
  int b1;
};
class B2 : virtual public A {
  int b2;
};
class C : public B1, public B2 {
  int c;
};

int main() {
  A obj1; B1 obj2; B2 obj3; C obj4;
  cout << "sizeof(A)  = " << sizeof(obj1) << endl;
  cout << "sizeof(B1) = " << sizeof(obj2) << endl;
  cout << "sizeof(B2) = " << sizeof(obj3) << endl;
  cout << "sizeof(C)  = " << sizeof(obj4) << endl;
  return 0;
}

Распечатки:

sizeof(A)  = 20
sizeof(B1) = 32
sizeof(B2) = 32
sizeof(C)  = 56

Теперь более понятно, что sizeof(C)sizeof(B1) + sizeof(B2).

Чтобы узнать больше, вы можете поиграть со смещениями, чтобы увидеть, как именно расположено C.

person rustyx    schedule 08.01.2021