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

У меня есть два класса - один базовый класс и один производный от него:

class base {

 int i ;

  public :
  virtual ~ base () { }
};

class derived :  virtual public base { int j ; };

main()

{ cout << sizeof ( derived ) ; }

Здесь ответ равен 16. Но если я вместо этого сделаю невиртуальное публичное наследование или сделаю базовый класс неполиморфным, то я получу ответ как 12, т.е. если я сделаю:

class base {

 int i ;

 public :
virtual ~ base () { }
};

class derived :  public base { int j ; };

main()

{ cout << sizeof ( derived ) ; }

OR

class base {

int i ;

public :
~ base () { }
};

class derived :  virtual public base { int j ; };

main()

{ cout << sizeof ( derived ) ; }

В обоих случаях ответ 12.

Может кто-нибудь объяснить, почему существует разница в размере производного класса в 1-м и двух других случаях?

(Я работаю над code::blocks 10.05, если кому-то это действительно нужно)


person cirronimbo    schedule 05.06.2012    source источник
comment
Пожалуйста, отформатируйте код.   -  person Luchian Grigore    schedule 05.06.2012
comment
Поставьте четыре пробела перед каждой строкой кода, чтобы сформировать блок кода. Добавьте еще четыре (или два) пробела для каждого отступа, чтобы ваш код был правильно отформатирован. Это значительно упростит чтение вашего кода.   -  person Alex Lockwood    schedule 05.06.2012
comment
Вы также можете выбрать код с помощью мыши, а затем щелкнуть значок { } на панели форматирования. Это сформирует для вас блок кода.   -  person Alex Lockwood    schedule 05.06.2012
comment
простите за недостатки. Думаю, теперь я их исправил.   -  person cirronimbo    schedule 05.06.2012
comment
Может кто-нибудь объяснить, что происходит? Хм, размер одного 16, а другого 12. Можете ли вы уточнить свой вопрос?   -  person Robᵩ    schedule 05.06.2012
comment
чтобы быть конкретным, я спрашиваю, почему такая разница в размере производного класса в этих трех случаях?   -  person cirronimbo    schedule 05.06.2012
comment
размер увеличивается из-за необходимости таблицы виртуальных функций (указателя), которая должна быть включена, чтобы получить полиморфизм времени выполнения.   -  person Chad    schedule 05.06.2012
comment
@Chad: Нет, в базе уже есть vptr, и спрашивающий ясно это понимает и хочет знать, почему в производном есть еще один скрытый указатель, и только если он виртуально наследуется.   -  person abarnert    schedule 06.06.2012
comment
@cirronimbo: Вы понимаете, для чего нужно виртуальное наследование и как оно работает? Если нет, сначала изучите это. Если да, то если вы сообщите нам, какой компилятор вы используете, возможно, мы сможем точно объяснить, как он реализует виртуальное наследование.   -  person abarnert    schedule 06.06.2012
comment
@abarnert- Точно. вот что я спрашиваю. Только вы, кажется, понимаете это здесь. Я использую компилятор GNU GCC для Code::Blocks 10.05.   -  person cirronimbo    schedule 06.06.2012
comment
Кроме того, если я удалю элементы данных i и j из базовых и производных классов соответственно. т. е. пусть размеры определяются исключительно VPTR, тогда этого скрытого указателя больше не существует, и все работает так, как ожидалось, и во всех трех случаях выводится 4.   -  person cirronimbo    schedule 06.06.2012
comment
@cirronimbo: Если вы прочитаете мой ответ и (лучший) ответ Тимо ниже или, что еще лучше, погуглите Стэнли Липпмана «Внутри объектной модели C++», вы поймете, почему большинству платформ не нужен дополнительный скрытый указатель, если есть нет элементов данных.   -  person abarnert    schedule 06.06.2012
comment
@cirronimbo: PS, просто GNU GCC не помогает. Нам нужно знать версию gcc и целевую платформу — как ЦП, так и ОС (а для Windows — нативную/MinGW или Cygwin).   -  person abarnert    schedule 06.06.2012


Ответы (6)


Смысл виртуального наследования в том, чтобы разрешить совместное использование базовых классов. Вот проблема:

struct base { int member; virtual void method() {} };
struct derived0 : base { int d0; };
struct derived1 : base { int d1; };
struct join : derived0, derived1 {};
join j;
j.method();
j.member;
(base *)j;
dynamic_cast<base *>(j);

Последние 4 строки неоднозначны. Вы должны явно указать, хотите ли вы, чтобы база находилась внутри производного0 или база внутри производного1.

Если вы измените вторую и третью строку следующим образом, проблема исчезнет:

struct derived0 : virtual base { int d0; };
struct derived1 : virtual base { int d1; };

Ваш объект j теперь имеет только одну копию базы, а не две, поэтому последние 4 строки перестают быть двусмысленными.

Но подумайте, как это должно быть реализовано. Обычно в производном0 d0 идет сразу после m, а в производном1 d1 идет сразу после m. Но при виртуальном наследовании они оба имеют один и тот же m, поэтому вы не можете иметь сразу после него и d0, и d1. Так что вам понадобится какая-то форма дополнительной косвенности. Вот откуда берется дополнительный указатель.

Если вы хотите точно знать, что такое макет, это зависит от вашей целевой платформы и компилятора. Просто "gcc" недостаточно. Но для многих современных целей, отличных от Windows, ответ определяется через Itanium C++ ABI, который задокументирован по адресу http://mentorembedded.github.com/cxx-abi/abi.html#vtable.

person abarnert    schedule 05.06.2012
comment
спасибо абарнерт. Я думаю, мне не нужно сейчас рассказывать вам о характеристиках моей системы :) - person cirronimbo; 06.06.2012

Здесь есть две отдельные вещи, которые вызывают дополнительные накладные расходы.

Во-первых, наличие виртуальных функций в базовом классе увеличивает его размер на размер указателя (в данном случае 4 байта), поскольку ему необходимо хранить указатель на таблицу виртуальных методов:

normal inheritance with virtual functions:

0        4       8       12
|      base      |
| vfptr  |  i    |   j   |

Во-вторых, при виртуальном наследовании в derived требуется дополнительная информация, чтобы можно было определить местонахождение base. При обычном наследовании смещение между derived и base является константой времени компиляции (0 для одиночного наследования). В виртуальном наследовании смещение может зависеть от типа среды выполнения и фактической иерархии типов объекта. Реализации могут различаться, но, например, Visual C++ делает это примерно так:

virtual inheritance with virtual functions:

0        4         8        12        16
                   |      base        |
|  xxx   |   j     |  vfptr |    i    |

Где xxx — указатель на информационную запись некоторого типа, позволяющую определить смещение до base.

И, конечно же, возможно виртуальное наследование без виртуальных функций:

virtual inheritance without virtual functions:

0        4         8        12
                   |  base  |
|  xxx   |   j     |   i    |
person Timo    schedule 05.06.2012
comment
Поскольку он специально спрашивал о gcc, возможно, было бы лучше узнать целевую платформу и нарисовать подробности об этой платформе, а не о MSVC, но в остальном это отличный ответ — очень простой и понятный. - person abarnert; 06.06.2012
comment
В этом случае, если (в виртуальном наследовании с виртуальными функциями) я удаляю элементы данных i и j из базового и производного классов соответственно, тогда размер производного класса должен быть 8 (xxx + vfptr), НО его выход в быть только 4 ?? - person cirronimbo; 06.06.2012
comment
Детали будут зависеть от вашего компилятора и платформы. Но обратите внимание, что если вы удалили члены данных, нет необходимости различать часть производный0 и часть производный1 — в обоих случаях данные (которых нет) идут сразу после базового vptr, поэтому вы не можете t нужны оба указателя. - person abarnert; 06.06.2012
comment
@abaenert: я разместил это до того, как прочитал ваш пост. Получил идею сейчас. - person cirronimbo; 06.06.2012

Если у класса есть какая-либо виртуальная функция, объекты этого класса должны иметь vptr, то есть указатель на vtable, то есть виртуальную таблицу, из которой можно найти адрес правильной виртуальной функции. Вызываемая функция зависит от динамического типа объекта, который является наиболее производным классом, базовым подобъектом которого является объект.

Поскольку производный класс виртуально наследуется от базового класса, расположение базового класса относительно производного класса не является фиксированным, оно также зависит от динамического типа объекта. С gcc класс с виртуальными базовыми классами нуждается в vptr для поиска базовых классов (даже если виртуальной функции нет).

Кроме того, базовый класс содержит член данных, который расположен сразу после базового класса vptr. Схема памяти базового класса: { vptr, int }

Если базовому классу требуется vptr, производному от него классу также потребуется vptr, но часто повторно используется «первый» vptr подобъекта базового класса (этот базовый класс с повторно используемым vptr называется первичной базой). Однако в данном случае это невозможно, потому что производному классу нужен vptr не только для определения того, как вызывать виртуальную функцию, но и для того, чтобы определить, где находится виртуальная база. Производный класс не может найти свой виртуальный базовый класс без использования vptr; если виртуальный базовый класс использовался как первичный, производный класс должен был найти первичный базовый класс, чтобы прочитать vptr, и должен был прочитать vptr, чтобы найти первичный базовый элемент.

Таким образом, у производного не может быть первичной базы, и он вводит свой собственный vptr.

Макет подобъекта базового класса типа derived выглядит следующим образом: { vptr, int } с vptr, указывающим на виртуальную таблицу для производных, содержащую не только адреса виртуальных функций , но и относительное расположение всех его виртуальных базовых классов (здесь просто base), представленное как смещение.

Макет полного объекта типа derived выглядит следующим образом: { подобъект базового класса типа derived, base }

Таким образом, минимальный возможный размер derived составляет (2 int + 2 vptr) или 4 слова в обычных архитектурах ptr = int = word, или 16 байт в данном случае. (А Visual C++ создает большие объекты (когда задействованы виртуальные базовые классы), я полагаю, что derived будет иметь еще один указатель.)

Так что да, виртуальные функции имеют свою цену, и виртуальное наследование имеет свою цену. Затраты памяти на виртуальное наследование в этом случае составляют еще один указатель на объект.

В проектах со многими виртуальными базовыми классами затраты памяти на объект могут быть пропорциональны количеству виртуальных базовых классов или нет; нам нужно будет обсудить конкретные иерархии классов, чтобы оценить стоимость.

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

person curiousguy    schedule 05.08.2012
comment
При записи объектов в файл я напрямую записываю весь объект в файл с помощью функции write(). Таким образом, виртуальный добавляет еще 4 байта (или еще 8 байтов в зависимости от архитектуры) при записи. Есть ли способ избежать этого? - person Rajesh; 15.11.2017
comment
Запись байтов представления в файлы вызывает множество проблем. Как вы хотите прочитать объект? - person curiousguy; 16.11.2017

Что происходит, так это дополнительные накладные расходы, используемые для пометки класса как имеющего виртуальные члены или включающего виртуальное наследование. Сколько лишнего зависит от компилятора.

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

person David Hammen    schedule 05.06.2012

Возможно, дополнительные 4 байта необходимы для обозначения типа класса во время выполнения. Например:

class A {
 virtual int f() { return 2; }
}

class B : virtual public A {
 virtual int f() { return 3; }
}

int call_function( A *a) {
   // here we don't know what a really is (A or B)
   // because of this to call correct method
   // we need some runtime knowledge of type and storage space to put it in (extra 4 bytes).
   return a->f();
}

int main() {
   B b;
   A *a = (A*)&b;

   cout << call_function(a);
}
person Ruben    schedule 05.06.2012
comment
Моя система показывает размер этого VPTR, т. е. указателя типа void *, как 4 байта (при условии, что 4 байта, на которые вы ссылаетесь, являются VPTR), а размер int также 4 байта. Итак, в моем первом случае размер производного класса должен быть только 12. Но выходит 16. Так откуда берутся эти 4 лишних байта, вот что такое маскировка. - person cirronimbo; 05.06.2012
comment
Похоже, VPTR добавляется дважды - для базового и для производного. - person Ruben; 06.06.2012
comment
Точно. На этом я застрял, потому что ничего подобного раньше не читал! - person cirronimbo; 06.06.2012
comment
Это не совсем объясняет, почему размер 16! - person curiousguy; 05.08.2012

Дополнительный размер связан с указателем vtable/vtable, который «невидимо» добавляется к вашему классу, чтобы удерживать указатель функции-члена для определенного объекта этого класса или его потомка/предка.

Если это неясно, вам нужно больше читать о виртуальном наследовании в C++.

person kfh    schedule 05.06.2012
comment
Он четко понимает, что там 4-х байтный VPTR. Он спрашивает, почему у B есть дополнительные 4 байта поверх 4, которые есть у обоих. И это из-за того, как работает виртуальное наследование, о котором ваш ответ не говорит. - person abarnert; 06.06.2012