У каждого объекта виртуального класса есть указатель на vtable?

У каждого объекта виртуального класса есть указатель на vtable?

Или это только объект базового класса с виртуальной функцией?

Где хранилась таблица vtable? раздел кода или раздел данных процесса?

Ответ 1

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

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

Обратите внимание, что ни один из них не задан языком С++ - реализация может обрабатывать виртуальную диспетчеризацию каким-либо другим способом, если захочет. Однако это реализация, которая используется каждым компилятором, с которым я знаком. Книга Стэна Липпмана "Внутри объектной модели С++" описывает, как это работает очень хорошо.

Ответ 2

Как и кто-то другой, стандарт С++ не предоставляет виртуальную таблицу методов, но позволяет использовать ее. Я провел тесты с использованием gcc и этого кода и одним из самых простых сценариев:

class Base {
public: 
    virtual void bark() { }
    int dont_do_ebo;
};

class Derived1 : public Base {
public:
    virtual void bark() { }
    int dont_do_ebo;
};

class Derived2 : public Base {
public:
    virtual void smile() { }
    int dont_do_ebo;
};

void use(Base* );

int main() {
    Base * b = new Derived1;
    use(b);

    Base * b1 = new Derived2;
    use(b1);
}

Добавлены данные-члены, чтобы компилятор не предоставил базовому классу нулевой размер (он известен как оптимизация с пустым-базовым классом). Это макет, который выбрал GCC: (печать с использованием -fdump-class-hierarchy)

Vtable for Base
Base::_ZTV4Base: 3u entries
0     (int (*)(...))0
4     (int (*)(...))(& _ZTI4Base)
8     Base::bark

Class Base
   size=8 align=4
   base size=8 base align=4
Base (0xb7b578e8) 0
    vptr=((& Base::_ZTV4Base) + 8u)

Vtable for Derived1
Derived1::_ZTV8Derived1: 3u entries
0     (int (*)(...))0
4     (int (*)(...))(& _ZTI8Derived1)
8     Derived1::bark

Class Derived1
   size=12 align=4
   base size=12 base align=4
Derived1 (0xb7ad6400) 0
    vptr=((& Derived1::_ZTV8Derived1) + 8u)
  Base (0xb7b57ac8) 0
      primary-for Derived1 (0xb7ad6400)

Vtable for Derived2
Derived2::_ZTV8Derived2: 4u entries
0     (int (*)(...))0
4     (int (*)(...))(& _ZTI8Derived2)
8     Base::bark
12    Derived2::smile

Class Derived2
   size=12 align=4
   base size=12 base align=4
Derived2 (0xb7ad64c0) 0
    vptr=((& Derived2::_ZTV8Derived2) + 8u)
  Base (0xb7b57c30) 0
      primary-for Derived2 (0xb7ad64c0)

Как вы видите, у каждого класса есть vtable. Первые две записи являются особенными. Второй указывает на данные RTTI класса. Первый - я знал это, но забыл. Это было полезно в более сложных случаях. Ну, как показывает макет, если у вас есть объект класса Derived1, то vptr (v-table-pointer) будет указывать на v-таблицу класса Derived1, конечно, которая имеет ровно одну запись для своей коры функции, указывающей на Версия Derived1. Derived2 vptr указывает на Derived2 vtable, который имеет две записи. Другой - новый метод, добавленный им, улыбается. Он повторяет запись для Base:: bark, которая будет указывать на базовую версию функции, конечно, потому что она является самой производной версией.

Я также сбрасывал дерево, которое генерировалось GCC после выполнения некоторых оптимизаций (конструктор inlined,...), с оптимизацией -fdump-tree. На выходе используется язык среднего класса GCC GIMPL, который независим от интерфейса, с отступом в некоторую структурную структуру C-типа:

;; Function virtual void Base::bark() (_ZN4Base4barkEv)
virtual void Base::bark() (this)
{
<bb 2>:
  return;
}

;; Function virtual void Derived1::bark() (_ZN8Derived14barkEv)
virtual void Derived1::bark() (this)
{
<bb 2>:
  return;
}

;; Function virtual void Derived2::smile() (_ZN8Derived25smileEv)
virtual void Derived2::smile() (this)
{
<bb 2>:
  return;
}

;; Function int main() (main)
int main() ()
{
  void * D.1757;
  struct Derived2 * D.1734;
  void * D.1756;
  struct Derived1 * D.1693;

<bb 2>:
  D.1756 = operator new (12);
  D.1693 = (struct Derived1 *) D.1756;
  D.1693->D.1671._vptr.Base = &_ZTV8Derived1[2];
  use (&D.1693->D.1671);
  D.1757 = operator new (12);
  D.1734 = (struct Derived2 *) D.1757;
  D.1734->D.1682._vptr.Base = &_ZTV8Derived2[2];
  use (&D.1734->D.1682);
  return 0;    
}

Как мы видим, он просто устанавливает один указатель - vptr - который укажет на соответствующую vtable, которую мы видели ранее при создании объекта. Я также сбрасывал код ассемблера для создания Derived1 и вызывается для использования ($ 4 - регистр первого аргумента, $2 - регистр возвращаемого значения, $0 всегда - 0-регистр) после того, как деманлинг имен в нем с помощью c++filt tool:)

      # 1st arg: 12byte
    add     $4, $0, 12
      # allocate 12byte
    jal     operator new(unsigned long)    
      # get ptr to first function in the vtable of Derived1
    add     $3, $0, vtable for Derived1+8  
      # store that pointer at offset 0x0 of the object (vptr)
    stw     $3, $2, 0
      # 1st arg is the address of the object
    add     $4, $0, $2
    jal     use(Base*)

Что произойдет, если мы хотим называть bark?:

void doit(Base* b) {
    b->bark();
}

Код GIMPL:

;; Function void doit(Base*) (_Z4doitP4Base)
void doit(Base*) (b)
{
<bb 2>:
  OBJ_TYPE_REF(*b->_vptr.Base;b->0) (b) [tail call];
  return;
}

OBJ_TYPE_REF - это конструкция GIMPL, которая довольно печатается (она документирована в gcc/tree.def в исходном коде gcc SVN)

OBJ_TYPE_REF(<first arg>; <second arg> -> <third arg>)

Значение: Используйте выражение *b->_vptr.Base для объекта b и сохраните специфическое значение frontend (С++) 0 (это индекс в таблице vtable). Наконец, он передает b в качестве аргумента "this". Можно ли назвать функцию, которая появляется во втором индексе в таблице vtable (обратите внимание, что мы не знаем, какую vtable этого типа!), GIMPL будет выглядеть так:

OBJ_TYPE_REF(*(b->_vptr.Base + 4);b->1) (b) [tail call];

Конечно, здесь снова появляется код сборки (отсекация кадров стека):

  # load vptr into register $2 
  # (remember $4 is the address of the object, 
  #  doit first arg)
ldw     $2, $4, 0
  # load whatever is stored there into register $2
ldw     $2, $2, 0
  # jump to that address. note that "this" is passed by $4
jalr    $2

Помните, что vptr точно указывает на первую функцию. (Перед этой записью был сохранен слот RTTI). Итак, все, что появляется в этом слоте, называется. Это также указывает на вызов как вызов хвоста, потому что это происходит как последнее утверждение в нашей функции doit.

Ответ 3

Vtable - это экземпляр класса, то есть если у меня есть 10 объектов класса, у которого есть виртуальный метод, существует только одна vtable, которая разделяется между всеми 10 объектами.

Все 10 объектов в этом случае указывают на ту же таблицу vtable.

Ответ 4

Попробуйте это дома:

#include <iostream>
struct non_virtual {}; 
struct has_virtual { virtual void nop() {} }; 
struct has_virtual_d : public has_virtual { virtual void nop() {} }; 

int main(int argc, char* argv[])
{
   std::cout << sizeof non_virtual << "\n" 
             << sizeof has_virtual << "\n" 
             << sizeof has_virtual_d << "\n";
}

Ответ 5

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

НО: Все общие компиляторы (т.е. те, о которых я знаю) используют VTabels.
Тогда да. Любой класс, имеющий виртуальный метод или полученный из класса (прямо или косвенно), который имеет виртуальный метод, будет иметь объекты с указателем на VTable.

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

Ответ 6

Все виртуальные классы обычно имеют vtable, но это не требуется стандартом С++, и метод хранения зависит от компилятора.

Ответ 7

Чтобы ответить на вопрос о том, какие объекты (экземпляры с этого момента) имеют vtables и где, полезно подумать, когда вам нужен указатель vtable.

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

class A { virtual void f(); int a; };
class B: public A { virtual void f(); virtual void g(); int b; };
class C: public B { virtual void f(); virtual void g(); virtual void h(); int c; };
class D: public A { virtual void f(); int d; };
class E: public B { virtual void f(); int e; };

В результате вам понадобятся пять vtables: A, B, C, D и E нуждаются в собственных vtables.

Затем вам нужно знать, какую виртуальную таблицу использовать, используя указатель или ссылку на определенный класс. Например, учитывая указатель на A, вам нужно знать достаточно о макете A, чтобы вы могли получить таблицу vtable, которая сообщает вам, куда отправлять A:: f(). Учитывая указатель на B, вам нужно знать достаточно о макете B для отправки B:: f() и B:: g(). И так далее и т.д.

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

A vtable;
int a;

И экземпляр B будет:

A vtable;
int a;
B vtable;
int b;

И вы можете создать правильный виртуальный диспетчерский код из этого макета.

Вы также можете оптимизировать компоновку, объединив vtable-указатели vtables, которые имеют один и тот же макет, или один из подмножеств другого. Таким образом, в приведенном выше примере вы можете также разбить B как:

B vtable;
int a;
int b;

Поскольку B vtable является надмножеством A. B vtable имеет записи для A:: f и B:: g, а A vtable имеет записи для A:: f.

Для полноты, вот как вы могли бы отображать все vtables, которые мы видели до сих пор:

A vtable: A::f
B vtable: A::f, B::g
C vtable: A::f, B::g, C::h
D vtable: A::f
E vtable: A::f, B::g

И фактические записи будут:

A vtable: A::f
B vtable: B::f, B::g
C vtable: C::f, C::g, C::h
D vtable: D::f
E vtable: E::f, B::g

При множественном наследовании вы выполняете тот же анализ:

class A { virtual void f(); int a; };
class B { virtual void g(); int b; };
class C: public A, public B { virtual void f(); virtual void g(); int c; };

И результирующие макеты будут такими:

A: 
A vtable;
int a;

B:
B vtable;
int b;

C:
C A vtable;
int a;
C B vtable;
int b;
int c;

Вам нужен указатель на vtable, совместимый с A, и указатель на vtable, совместимый с B, потому что ссылка на C может быть преобразована в ссылку A или B, и вам нужно отправить виртуальные функции на C.

Из этого вы можете видеть, что количество указателей vtable, которое имеет конкретный класс, - это, по крайней мере, число корневых классов, из которых он происходит (напрямую или из-за суперкласса). Корневой класс - это класс, который имеет vtable, который не наследует от класса, который также имеет vtable.

Виртуальное наследование порождает еще один бит косвенности в миксе, но вы можете использовать одну и ту же метрику для определения количества указателей vtable.

Ответ 8

Каждый объект полиморфного типа будет иметь указатель на Vtable.

Где хранится VTable, зависит от компилятора.

Ответ 9

Не обязательно

Практически каждый объект, имеющий виртуальную функцию, будет иметь один указатель v-таблицы. Для каждого класса, который имеет виртуальную функцию, из которой происходит объект, не требуется указатель v-таблицы.

Новые компиляторы, которые в достаточной степени анализируют код, могут устранить v-таблицы в некоторых случаях.

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

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

Итак, в таких случаях v-таблица не нужна, и объекты могут не иметь ее.