A таблицы виртуальных методов (VMT ), таблицы виртуальных функций, таблицы виртуальных вызовов, таблицы диспетчеризации, vtable или vftable - это механизм, используемый в языке программирования для поддержки динамической диспетчеризации (или времени выполнения метод привязка ).
Всякий раз, когда класс определяет виртуальную функцию (или метод), большинство компиляторов добавляют скрытую переменную-член к классу, которая указывает на массив указателей на (виртуальные) функции, называемые виртуальным методом. Таблица. Эти указатели используются во время выполнения для вызова соответствующих реализаций функций, потому что во время компиляции еще может быть неизвестно, должна ли быть вызвана базовая функция или производная, реализованная классом, наследуемым от базового класса.
Существует много разных способов реализации такой динамической диспетчеризации, но использование таблиц виртуальных методов особенно распространено среди C ++ и связанных языков (таких как D и C # ). Языки, которые отделяют программный интерфейс объектов от реализации, такие как Visual Basic и Delphi, также склонны использовать этот подход, поскольку он позволяет объектам использовать другую реализацию, просто используя другой набор указателей на методы.
Предположим, программа содержит три класса в иерархии наследования : суперкласс, Cat
и два подклассы, HouseCat
и Lion
. Класс Cat
определяет виртуальную функцию с именем Speak
, поэтому его подклассы могут обеспечивать соответствующую реализацию (например, meow
или roar
). Когда программа вызывает функцию , говорите
по ссылке Cat
(которая может относиться к экземпляру Cat
или экземпляру HouseCat
или Lion
), код должен уметь определять, в какую реализацию функции следует направить вызов. Это зависит от фактического класса объекта, а не от класса ссылки на него (Cat
). Класс обычно не может быть определен статически (то есть во время компиляции ), поэтому компилятор не может решить, какую функцию вызвать в это время. Вместо этого вызов должен быть отправлен в нужную функцию динамически (то есть во время выполнения ).
Таблица виртуальных методов объекта будет содержать адреса динамически связанных методов объекта. Вызов метода выполняется путем получения адреса метода из таблицы виртуальных методов объекта. Таблица виртуальных методов одинакова для всех объектов, принадлежащих к одному классу, и поэтому обычно используется ими совместно. Объекты, принадлежащие к типо-совместимым классам (например, братьям и сестрам в иерархии наследования) будут иметь таблицы виртуальных методов с одинаковым макетом: адрес данного метода будет отображаться с одинаковым смещением для всех типов-совместимых классов. Таким образом, выборка адреса метода из заданного смещения в таблицу виртуальных методов приведет к получению метода, соответствующего фактическому классу объекта.
Стандарты C ++ не определяют, как именно должна быть динамическая отправка реализовано, но компиляторы обычно используют незначительные вариации той же базовой модели.
Обычно компилятор создает отдельную таблицу виртуальных методов для каждого класса. Когда объект создается, указатель на эту таблицу, называемый указателем виртуальной таблицы, vpointer или VPTR, добавляется как скрытый член этого объекта. Таким образом, компилятор также должен сгенерировать «скрытый» код в конструкторах каждого класса для инициализации указателя виртуальной таблицы нового объекта адресом таблицы виртуальных методов своего класса.
Многие компиляторы помещают указатель виртуальной таблицы в качестве последнего члена объекта; другие компиляторы ставят его первым; переносимый исходный код работает в любом случае. Например, g ++ ранее помещал указатель в конец объекта.
Рассмотрим следующие объявления классов в синтаксисе C ++ :
class B1 {public: virtual ~ B1 () {} void f0 () {} virtual void f1 () {} int int_in_b1; }; class B2 {public: virtual ~ B2 () {} virtual void f2 () {} int int_in_b2; };
используется для получения следующего класса:
класс D: общедоступный B1, общедоступный B2 {общественный: void d () {} void f2 () override {} int int_in_d; };
и следующий фрагмент кода C ++:
B2 * b2 = new B2 (); D * d = новый D ();
g ++ 3.4.6 из GCC создает следующую 32-битную схему памяти для объекта b2
:
b2: +0: указатель на таблицу виртуальных методов B2 +4: значение таблицы виртуальных методов int_in_b2 из B2: +0: B2 :: f2 ()
и следующий макет памяти для объекта d
:
d: +0: указатель на таблицу виртуальных методов D (для B1) +4: значение int_in_b1 +8: указатель на таблицу виртуальных методов D (для B2) +12: значение int_in_b2 +16: значение int_in_d Общий размер: 20 байт. таблица виртуальных методов D (для B1): +0: B1 :: f1 () // B1 :: f1 () не замещает таблицу виртуальных методов D (для B2): +0: D :: f2 () / / B2 :: f2 () заменяется D :: f2 ()
Обратите внимание, что те функции, которые не содержат ключевое слово virtual
в своем объявлении (например, f0 ()
и d ()
) обычно не появляются в таблице виртуальных методов. Существуют исключения для особых случаев, которые задаются конструктором по умолчанию .
. Также обратите внимание на виртуальные деструкторы в базовых классах, B1
и B2
. Они необходимы для того, чтобы delete d
мог освободить память не только для D
, но также для B1
и B2
, если d
- указатель или ссылка на типы B1
или B2
. Они были исключены из макетов памяти для простоты примера.
Переопределение метода f2 ()
в классе D
реализуется путем дублирования таблицы виртуальных методов B2
и замены указателя на B2 :: f2 ()
с указателем на D :: f2 ()
.
Компилятор g ++ реализует множественное наследование классов B1
и B2
в классе D
с использованием двух таблиц виртуальных методов, по одной для каждого базового класса. (Существуют и другие способы реализации множественного наследования, но это наиболее распространенный.) Это приводит к необходимости «исправлений указателя», также называемых преобразователями, когда приведение.
Рассмотрим следующее Код C ++:
D * d = new D (); B1 * b1 = d; B2 * b2 = d;
В то время как d
и b1
будут указывать на одну и ту же ячейку памяти после выполнения этого кода, b2
будет указывать на местоположение d + 8
(восемь байтов за пределами области памяти d
). Таким образом, b2
указывает на область в пределах d
, которая «выглядит как» экземпляр B2
, т. Е. Имеет ту же структуру памяти, что и экземпляр B2
.
Вызов d->f1 ()
обрабатывается разыменованием d
vpointer D :: B1
, поиск запись f1
в таблице виртуальных методов, а затем разыменование этого указателя для вызова кода.
В случае одиночного наследования (или в языке с единственным наследованием), если vpointer всегда является первым элементом в d
(как и во многих компиляторах), это уменьшает на следующий псевдо-C ++:
(* ((* d) [0])) (d)
Где * d относится к таблице виртуальных методов D, а [0] относится к первому методу в таблица виртуальных методов. Параметр d становится указателем «this» на объект.
В более общем случае вызов B1 :: f1 ()
или D :: f2 ()
более сложен:
(* (* ( d [+0] / * указатель на таблицу виртуальных методов D (для B1) * /) [0])) (d) / * Вызов d->f1 () * / (* (* (d [+8] / * указатель на таблицу виртуальных методов D (для B2) * /) [0])) (d + 8) / * Вызов d->f2 () * /
Вызов d->f1 () проходит указатель B1 в качестве параметра. Вызов d->f2 () передает указатель B2 в качестве параметра. Этот второй вызов требует исправления для создания правильного указателя. Местоположение B2 :: f2 отсутствует в таблице виртуальных методов для D.
Для сравнения, вызов d->f0 ()
намного проще:
(* B1 :: f0) (d)
Виртуальный вызов требует, по крайней мере, дополнительного индексированного разыменования, а иногда и добавления «исправления» по сравнению с невиртуальным вызовом, который представляет собой просто переход к встроенный указатель. Следовательно, вызов виртуальных функций по своей сути медленнее, чем вызов не виртуальных функций. Эксперимент, проведенный в 1996 году, показывает, что примерно 6–13% времени выполнения тратится просто на отправку правильной функции, хотя накладные расходы могут достигать 50%. Стоимость виртуальных функций может быть не такой высокой на современных архитектурах CPU из-за гораздо большего размера кешей и лучшего предсказания ветвлений.
Кроме того, в средах, где JIT-компиляция не выполняется при использовании вызовы виртуальных функций обычно не могут быть встроенными. В некоторых случаях компилятор может выполнить процесс, известный как девиртуализация, в котором, например, поиск и косвенный вызов заменяются условным выполнением каждого встроенного тела, но такие оптимизации не являются общими.
Чтобы избежать этих накладных расходов, компиляторы обычно избегают использования таблиц виртуальных методов всякий раз, когда вызов может быть разрешен во время времени компиляции.
Таким образом, для вызова f1
выше может не потребоваться поиск в таблице, потому что компилятор может определить, что d
может содержать только D
в этот момент, а D
не отменяет f1
. Или компилятор (или оптимизатор) может обнаружить, что в программе нет подклассов B1
, которые переопределяют f1
. Вызов B1 :: f1
или B2 :: f2
, вероятно, не потребует поиска в таблице, потому что реализация указана явно (хотя он все еще требует исправления указателя this.).
Таблица виртуальных методов, как правило, является хорошим компромиссом для достижения динамической диспетчеризации, но есть альтернативы, например, с более высокой производительностью, но с другой стоимостью.
Однако таблицы виртуальных методов допускают только одиночную отправку для специального параметра this, в отличие от множественной отправки (как в CLOS или Дилан ), где типы всех параметров могут быть учтены при диспетчеризации.
Таблицы виртуальных методов также работают, только если диспетчеризация ограничена известным набором методов, поэтому их можно поместить в простой массив, построенный во время компиляции, в отличие от duck typing languages ( например Smalltalk, Python или JavaScript ).
Языки, которые предоставляют одну или обе эти функции, часто отправляются путем поиска строки в хэш-таблице или каким-либо другим эквивалентным методом. Существует множество способов сделать это быстрее (например, интернирование / разметка имен методов, кэширование запросов, своевременная компиляция ).
-fdump-class -ierarchy
(начиная с версии 8: -fdump-lang-class
) можно использовать для дампа таблиц виртуальных методов для ручной проверки. Для компилятора AIX VisualAge XlC используйте -qdump_class_hierarchy
для создания дампа иерархии классов и макета таблицы виртуальных функций.