У каждого объекта виртуального класса есть указатель на vtable? - PullRequest
10 голосов
/ 18 февраля 2009

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

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

Где хранилась виртуальная таблица? секция кода или секция данных процесса?

Ответы [ 9 ]

15 голосов
/ 18 февраля 2009

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

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

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

12 голосов
/ 18 февраля 2009

Как сказал кто-то еще, стандарт C ++ не предписывает таблицу виртуальных методов, но позволяет использовать ее. Я провел свои тесты, используя 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 -ierarchy)

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. Vptr Derived2 указывает на vtable Derived2, который имеет две записи. Другой - новый метод, добавленный им, улыбка. Он повторяет запись для Base :: bark, которая, конечно, будет указывать на версию функции Base, потому что это самая производная версия.

Я также выкинул дерево, сгенерированное GCC после некоторой оптимизации (конструктор встроен, ...), с -fdump-tree-optimized. Выходные данные используют язык среднего уровня 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 инструмент:)

      # 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 и сохраните определенное значение внешнего интерфейса (c ++) 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's 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 хранился). Итак, все, что появляется в этом слоте, называется. Он также помечает вызов как tail-call, потому что это происходит как последний оператор в нашей функции doit.

4 голосов
/ 18 февраля 2009

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

#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";
}
4 голосов
/ 18 февраля 2009

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

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

2 голосов
/ 18 февраля 2009

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

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

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

1 голос
/ 18 февраля 2009

Чтобы ответить на вопрос о том, какие объекты (экземпляры отныне) имеют 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, чтобы можно было получить виртуальную таблицу, сообщающую, куда отправлять A :: f (). Имея указатель на B, вам нужно знать достаточно о компоновке B, чтобы отправить B :: f () и B :: g (). И так далее и тому подобное.

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

A's vtable;
int a;

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

A's vtable;
int a;
B's vtable;
int b;

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

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

B's vtable;
int a;
int b;

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

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

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

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

A's vtable: A::f
B's vtable: B::f, B::g
C's vtable: C::f, C::g, C::h
D's vtable: D::f
E's 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's vtable;
int a;

B:
B's vtable;
int b;

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

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

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

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

1 голос
/ 18 февраля 2009

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

0 голосов
/ 18 февраля 2009

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

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

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

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

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

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

0 голосов
/ 18 февраля 2009

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

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

...