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

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

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

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

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

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

Ответ 1

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

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

  • Более правильным способом отображения внутреннего представления объекта является:

    | 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!

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

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

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

    C будет иметь группу vtable из двух vtables. Он будет делиться одним vtable с помощью A (vtable, где собственные функции текущего класса go называются "primary" ), и будет добавлена ​​таблица vtable для 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 перед каждой виртуальной таблицей в группе, и вы будете иметь приблизительную оценку того, как данные лежат внутри объекта. Вы можете прочитать об этом в соответствующем разделе двоичного стандарта GCC.

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

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

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

Ответ 2

  • Это кажется правильным для меня. Это не так, как если бы вы использовали указатель A, вам нужно только то, что A обеспечивает плюс, возможно, функции B-функций, доступные из A vtable (может быть несколько vtable, в зависимости от сложности компилятора и иерархии).
  • Я бы сказал, да, но это зависит от реализации компилятора, поэтому вам не нужно знать об этом.
  • и 4. Прочтите дальше.

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

Ответ 3

  • Если объект B наследуется от A, тогда представление памяти для B будет следующим:

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

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

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

  • Взгляните на этот на примере vtable layour при работе с множественным наследованием

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