stringtranslate.com

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

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

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

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

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

Выполнение

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

Стандарты C++ не определяют, как именно должна быть реализована динамическая диспетчеризация, но компиляторы обычно используют незначительные изменения одной и той же базовой модели.

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

Многие компиляторы помещают указатель виртуальной таблицы в качестве последнего члена объекта; другие компиляторы помещают его в качестве первого; переносимый исходный код работает в любом случае. [3] Например, g++ ранее помещал указатель в конец объекта. [4]

Пример

Рассмотрим следующие объявления классов в синтаксисе C++ :

класс B1 { public : virtual ~ B1 () {} void fnonvirtual () {} virtual void f1 () {} int int_in_b1 ; };              класс B2 { public : virtual ~ B2 () {} virtual void f2 () {} int int_in_b2 ; };           

используется для вывода следующего класса:

класс D : public B1 , public B2 { public : void d () {} void f2 () override {} int int_in_d ; };                

и следующий фрагмент кода C++:

B2 * b2 = новый B2 (); D * d = новый D ();        

g++ 3.4.6 из GCC создает следующую 32-битную структуру памяти для объекта b2: [примечание 1]

б2: +0: ​​указатель на таблицу виртуальных методов B2 +4: значение int_in_b2таблица виртуальных методов B2: +0: ​​B2::f2() 

и следующая структура памяти для объекта 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() // Расположение B2::f2 отсутствует в таблице виртуальных методов для D

Обратите внимание, что те функции, которые не содержат ключевое слово virtualв своем объявлении (например, fnonvirtual()и d()), обычно не отображаются в таблице виртуальных методов. Существуют исключения для особых случаев, как это предусмотрено конструктором по умолчанию .

Также обратите внимание на виртуальные деструкторы в базовых классах B1и B2. Они необходимы для обеспечения delete dосвобождения памяти не только для D, но и для B1и B2, если dэто указатель или ссылка на типы B1или B2. Они были исключены из макетов памяти для простоты примера. [примечание 2]

Переопределение метода f2() в классе Dреализуется путем дублирования таблицы виртуальных методов B2и замены указателя на B2::f2()указателем на D::f2().

Множественное наследование и thunks

Компилятор g++ реализует множественное наследование классов B1и B2в классе Dс помощью двух таблиц виртуальных методов, по одной для каждого базового класса. (Существуют и другие способы реализации множественного наследования, но этот является наиболее распространенным.) Это приводит к необходимости «исправления указателей», также называемых thunks , при приведении типов .

Рассмотрим следующий код C++:

D * d = новый 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++:

( * (( * д )[ 0 ]))( д )

Где *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->fnonvirtual()гораздо проще:

( * B1 :: fnonvirtual )( d )

Эффективность

Виртуальный вызов требует как минимум дополнительного индексированного разыменования и иногда добавления "fixup" по сравнению с невиртуальным вызовом, который является просто переходом к скомпилированному указателю. Поэтому вызов виртуальных функций по своей сути медленнее, чем вызов невиртуальных функций. Эксперимент, проведенный в 1996 году, показывает, что примерно 6–13% времени выполнения тратится просто на диспетчеризацию правильной функции, хотя накладные расходы могут достигать 50%. [5] Стоимость виртуальных функций может быть не такой высокой на современных архитектурах ЦП из-за гораздо большего кэша и лучшего предсказания ветвлений .

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

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

Таким образом, вызов f1выше может не потребовать поиска в таблице, поскольку компилятор может определить, что в этой точке dможет содержаться только , и не переопределяет . Или компилятор (или оптимизатор) может обнаружить, что в программе нет подклассов , которые переопределяют . Вызов или , вероятно, не потребует поиска в таблице, поскольку реализация указана явно (хотя она все еще требует исправления указателя 'this').DDf1B1f1B1::f1B2::f2

Сравнение с альтернативами

Таблица виртуальных методов, как правило, является хорошим компромиссом производительности для достижения динамической диспетчеризации, но существуют альтернативы, такие как диспетчеризация двоичного дерева, с более высокой производительностью в некоторых типичных случаях, но с другими компромиссами. [1] [6]

Однако таблицы виртуальных методов допускают только однократную диспетчеризацию по специальному параметру «this», в отличие от множественной диспетчеризации (как в CLOS , Dylan или Julia ), где типы всех параметров могут учитываться при диспетчеризации.

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

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

Смотрите также

Примечания

  1. ^ Аргумент G++ -fdump-class-hierarchy(начиная с версии 8: -fdump-lang-class) может использоваться для дампа таблиц виртуальных методов для ручной проверки. Для компилятора AIX VisualAge XlC используйте -qdump_class_hierarchyдля дампа иерархии классов и макета таблицы виртуальных функций.
  2. ^ "C++ - почему в виртуальной таблице два виртуальных деструктора и где находится адрес невиртуальной функции (gcc4.6.3)".

Ссылки

  1. ^ аб Зендра, Оливье; Колнет, Доминик; Коллин, Сюзанна (1997). Эффективная динамическая диспетчеризация без таблиц виртуальных функций: компилятор SmallEiffel - 12-я ежегодная конференция ACM SIGPLAN по системам, языкам и приложениям объектно-ориентированного программирования (OOPSLA'97), ACM SIGPLAN, октябрь 1997 г., Атланта, США. стр.125-141. инрия-00565627. Центр исследований и информатики кампуса Нанси, Батимент ЛОРИЯ. п. 16.
  2. Эллис и Страуструп 1990, стр. 227–232.
  3. ^ Дэнни Калев. «Справочное руководство по C++: Объектная модель II». 2003. Заголовки «Наследование и полиморфизм» и «Множественное наследование».
  4. ^ "C++ ABI Closed Issues". Архивировано из оригинала 25 июля 2011 г. Получено 17 июня 2011 г.{{cite web}}: CS1 maint: бот: исходный статус URL неизвестен ( ссылка )
  5. ^ Дризен, Карел и Хельцле, Урс, «Прямая стоимость вызовов виртуальных функций в C++», OOPSLA 1996
  6. ^ Зендра, Оливье и Дризен, Карел, «Структуры управления стресс-тестированием для динамической диспетчеризации в Java», стр. 105–118, Труды 2-го симпозиума USENIX по исследованиям и технологиям виртуальных машин Java, 2002 (JVM '02)