Таблица виртуальных методов

редактировать

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

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

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

Предположим, программа содержит три класса в иерархии наследования : суперкласс, Catи два подклассы, HouseCatи Lion. Класс Catопределяет виртуальную функцию с именем Speak, поэтому его подклассы могут обеспечивать соответствующую реализацию (например, meowили roar). Когда программа вызывает функцию , говоритепо ссылке Cat(которая может относиться к экземпляру Catили экземпляру HouseCatили Lion), код должен уметь определять, в какую реализацию функции следует направить вызов. Это зависит от фактического класса объекта, а не от класса ссылки на него (Cat). Класс обычно не может быть определен статически (то есть во время компиляции ), поэтому компилятор не может решить, какую функцию вызвать в это время. Вместо этого вызов должен быть отправлен в нужную функцию динамически (то есть во время выполнения ).

Содержание
  • 1 Реализация
  • 2 Пример
  • 3 Множественное наследование и переходы
  • 4 Вызов
  • 5 Эффективность
  • 6 Сравнение с альтернативами
  • 7 См. Также
  • 8 Примечания
  • 9 Ссылки
Реализация

Таблица виртуальных методов объекта будет содержать адреса динамически связанных методов объекта. Вызов метода выполняется путем получения адреса метода из таблицы виртуальных методов объекта. Таблица виртуальных методов одинакова для всех объектов, принадлежащих к одному классу, и поэтому обычно используется ими совместно. Объекты, принадлежащие к типо-совместимым классам (например, братьям и сестрам в иерархии наследования) будут иметь таблицы виртуальных методов с одинаковым макетом: адрес данного метода будет отображаться с одинаковым смещением для всех типов-совместимых классов. Таким образом, выборка адреса метода из заданного смещения в таблицу виртуальных методов приведет к получению метода, соответствующего фактическому классу объекта.

Стандарты 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 ()обрабатывается разыменованием dvpointer 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 ).

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

См. Также
Примечания
  1. ^G ++ -fdump-class -ierarchy(начиная с версии 8: -fdump-lang-class) можно использовать для дампа таблиц виртуальных методов для ручной проверки. Для компилятора AIX VisualAge XlC используйте -qdump_class_hierarchyдля создания дампа иерархии классов и макета таблицы виртуальных функций.
  2. ^https://stackoverflow.com/questions/17960917/why-there-are-two-virtual-destructor-in-the-virtual-table-and-where-is-address-o
Ссылки
  • Маргарет А. Эллис и Бьярн Страуструп (1990) Справочное руководство по C ++ с аннотациями. Ридинг, Массачусетс: Эддисон-Уэсли. (ISBN 0-201-51459-1 )

Последняя правка сделана 2021-06-18 03:36:51
Содержание доступно по лицензии CC BY-SA 3.0 (если не указано иное).
Обратная связь: support@alphapedia.ru
Соглашение
О проекте