Виртуальные таблицы и виртуальные указатели для множественного виртуального наследования и приведения типов - PullRequest
10 голосов
/ 24 июля 2010

Меня немного смущает vptr и представление объектов в памяти, и я надеюсь, что вы поможете мне лучше разобраться в этом вопросе.

  1. Рассмотрим B наследует от A иоба определяют виртуальные функции f().Из того, что я узнал, представление объекта класса B в памяти выглядит так: [ vptr | A | B ] и vtbl, на которые указывает vptr, содержит B::f().Я также понял, что приведение объекта от B к A ничего не делает, кроме игнорирования части B в конце объекта.Это правда?Разве это поведение не является неправильным?Мы хотим, чтобы этот объект типа A выполнял метод A::f(), а не B::f().

  2. Есть ли в системе число vtables как число классов?

  3. Как будет выглядеть vtable класса, который наследуется от двух или более классов?Как объект C будет представлен в памяти?

  4. То же, что вопрос 3, но с виртуальным наследованием.

Ответы [ 3 ]

16 голосов
/ 24 июля 2010

Следующее верно для GCC (и, кажется, верно для LLVM link ), но может также быть верно для используемого вами компилятора. Все это зависит от реализации и не регулируется стандартом C ++. Однако GCC пишет свой собственный двоичный стандартный документ, Itanium ABI .

Я попытался объяснить основные понятия о том, как виртуальные таблицы представлены в более простых словах, как часть моей статьи о производительности виртуальных функций в C ++ , которая может оказаться полезной. Вот ответы на ваши вопросы:

  1. Более правильный способ изобразить внутреннее представление объекта:

    | vptr | ======= | ======= |  <-- your object
           |----A----|         |
           |---------B---------|
    

    B содержит его базовый класс A, он просто добавляет пару своих собственных членов после его окончания.

    Приведение от B* к A* действительно ничего не делает, возвращает тот же указатель, а vptr остается тем же. Но, в двух словах, виртуальные функции не всегда вызываются через vtable . Иногда они вызываются как другие функции.

    Вот более подробное объяснение. Следует различать два способа вызова функции-члена:

    A a, *aptr;
    a.func();         // the call to A::func() is precompiled!
    aptr->A::func();  // ditto
    aptr->func();     // calls virtual function through vtable.
                      // It may be a call to A::func() or B::func().
    

    Дело в том, что во время компиляции известно , как будет вызываться функция: через vtable или просто будет обычным вызовом. И дело в том, что тип выражения приведения известен во время компиляции , и поэтому компилятор выбирает правильную функцию во время компиляции.

    B b, *bptr;          
    static_cast<A>(b)::func(); //calls A::func, because the type
       // of static_cast<A>(b) is A!
    

    В этом случае он даже не заглядывает внутрь vtable!

  2. Как правило, нет. Класс может иметь несколько vtables, если он наследует от нескольких баз, каждая из которых имеет свою собственную vtable. Такой набор виртуальных таблиц образует «группу виртуальных таблиц» (см. П. 3).

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

  3. Вот пример. Предположим, что C наследуется от A и B, каждый класс определяет virtual void func(), а также виртуальные функции a, b или c, соответствующие его имени.

    У C будет группа vtable из двух vtables. Он будет использовать одну виртуальную таблицу с A (виртуальная таблица, в которую входят собственные функции текущего класса, называется «основной»), и к ней добавляется виртуальная таблица для B:

    | C::func()   |   a()  |  c()  || C::func()  |   b()   |
    |---- vtable for A ----|        |---- vtable for B ----| 
    |--- "primary virtual table" --||- "secondary vtable" -|
    |-------------- virtual table group for C -------------|
    

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

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

    Из-за такого размещения виртуальные базы также вводят в свои vtables дополнительные элементы: vcall offset (для получения адреса конечной переопределения при переходе от указателя к виртуальной базе внутри законченного объекта в начале класса). который переопределяет виртуальную функцию) для каждой виртуальной функции, определенной там. Также каждая виртуальная база добавляет vbase смещений, которые вставляются в vtable производного класса; они позволяют определить, где начинаются данные виртуальной базы (их нельзя предварительно скомпилировать, поскольку фактический адрес зависит от иерархии: виртуальные базы находятся в конце объекта, а смещение от начала зависит от количества не виртуальных классы, которые наследует текущий класс.).

Гав, надеюсь, я не привнес много лишней сложности. В любом случае вы можете ссылаться на исходный стандарт или на любой документ собственного компилятора.

2 голосов
/ 24 июля 2010
  1. Мне кажется, это правильно.Это не так, как если бы вы использовали указатель A, вам нужно только то, что обеспечивает A плюс, может быть, реализации функций B, которые доступны из таблицы V (может быть несколько таблиц V, в зависимости от компилятора и сложности иерархии).
  2. Я бы сказал, что да, но это зависит от реализации компилятора, поэтому вам не нужно об этом знать.
  3. и 4. Читайте дальше.

IРекомендую прочитать Множественное наследование считается полезным , это длинная статья, но она проясняет предмет, так как подробно объясняет, как наследование работает в C ++ (ссылки на рисунки не работают, но онидоступны внизу страницы).

0 голосов
/ 24 июля 2010
  1. Если объект B наследуется от A, то представление памяти для B будет следующим:

    • указатель на виртуальную таблицу A
    • Определенные переменные / функции
    • указатель на виртуальную таблицу B
    • B специфические переменные / функции / переопределения

    Если у вас есть B * b = новый B (); (A) b-> f () затем:

    • если f объявлена ​​как виртуальная функция, то вызывается реализация B, потому что b имеет тип B
    • если f не был объявлен как виртуальная функция, то при вызове в vtable не будет поиска правильной реализации, и будет вызвана реализация A.
  2. Каждый объект будет иметь свою собственную vtable (не принимайте это как должное, поскольку я должен исследовать это

  3. Взгляните на this в качестве примера vtable Layer при работе с множественным наследованием

  4. См. this для обсуждения наследования алмазов и представления vtable

...