Генерация кода
Единичное наследование и виртуальные функции
Если класс base содержит виртуальную функцию vf, а класс derived, порожденный по классу base, также содержит функцию vf того же типа, то обращение к vf для объекта класса derived вызывает derived :: vf даже при доступе через указатель или ссылку на base. В таком случае говорят, что функция производного класса подменяет ( override ) функцию базового класса. Если, однако, типы этих функций различны, то функции считаются различными и механизм виртуальности не включается.
Виртуальные функции можно реализовать при помощи таблицы указателей на виртуальные функции vtbl. В случае единичного наследования таблица виртуальных функций класса будет содержать ссылки на соответствующие функции, а каждый объект данного класса будет содержать указатель на таблицу vtbl.
class A { public: int a; virtual void f(int); virtual void g(int); virtual void h(int); }; class B : public A { public: int b; void g(int); }; class C : public B { public: int c; void h(int); };
Объект класса C будет выглядеть примерно так:
Множественное наследование и виртуальные функции
При множественном наследовании виртуальные функции реализуются несколько сложнее. Рассмотрим следующие объявления:
class A { public: virtual void f(int); }; class B : { public: virtual void f(int); virtual void g(int); }; class C : public A, public B { public: void f(); };
Поскольку класс A порожден по классам A и B, каждый из следующих вызовов будет обращаться к C :: f() (считая, что каждый из трех указателей смотрит на объект класса C ):
pa -> f() pb -> f() pc -> f()
Рассмотрим, для примера, вызов pb -> f(). При входе в C :: f указатель this должен указывать на начало объекта C, а не на часть B в нем. Во время компиляции вообще говоря не известно, указывает ли pb на часть B в C. Например, из-за того, что pb может быть присвоен просто указатель на объект B. Так что величина delta(B), упомянутая выше, может быть различной для разных объектов в зависимости от структуры классов, порождаемых из B и должна где-то хранится во время выполнения.
Следовательно, delta(B) должно где-то храниться и быть доступно во время исполнения. Поскольку это смещение нужно только для виртуального вызова функции, логично хранить его в таблице виртуальных функций.
Указатель this, передаваемый виртуальной функции, может быть вычислен путем вычитания смещения объекта, для которого была определена виртуальная функция, из смещения объекта, для которого она вызвана, а затем вычитания этой разности из указателя, используемого при вызове. Здесь значение delta(B) будет необходимо для поиска начала объекта (в нашем случае C ), содержащего B, по указателю на B. Сгенерированный код вычтет значение delta(B) из значения указателя, так что хранится смещение со знаком минус, -delta(B). Объект класса C будет выглядеть следующим образом:
Таблица виртуальных функций vtbl для B в C отличается от vtbl для отдельно размещенного B. Каждая комбинация базового и производного классов имеет свою таблицу vtbl. В общем случае объект производного класса требует таблицу vtbl для каждого базового класса плюс таблицу для производного класса, не считая того, что производный класс может разделять таблицу vtbl со своим первым базовым классом. Таким образом, для объекта типа C в этом примере требуется две таблицы vtbl (таблица для A в C объединена с таблицей для C oбъекта и еще одна таблица нужна для B объекта в C ).
Виртуальные базовые классы с виртуальными функциями
При наличии виртуальных базовых классов построение таблиц для вызовов виртуальных функций становится более сложным. Рассмотрим следующие объявления:
class W { public: virtual void f(); virtual void g(); virtual void h(); virtual void k(); }; class MW : public virtual W { public: void g(); }; class BW : public virtual W { public: void f(); }; class BMW : public BW, public MW, public virtual W { public: void h(); };
Отношение наследования для этого примера может быть изображено в виде ациклического графа таким образом:
Функции-члены класса BMW могут использоваться, например, так:
void g(BMW¤pbmw) {pbmw ! f(); == вызывает BW :: f() pbmw ! g(); == вызывает MW :: g() pbmw ! h(); == вызывает BMW :: h() }
Рассмотрим теперь следующий вызов виртуальной функции f():
void h(BMW*pbmw) {MW*pmw = pbmw; pmw ! f(); == вызывает BW :: f(); потому, что // pbmw указывает на BMW, для которого f бер"тся из BW! }
Виртуальный вызов функции по одному пути в структуре наследования может привести к обращению к функции, переопределенной на другом пути.
Структура объектов класса BMW и его таблиц виртуальных функций vtbl могут выглядеть следующим образом:
Виртуальной функции должен быть передан указатель this на объект класса, в котором эта функция описана. Поэтому следует хранить смещение для каждого указателя функции из vtbl. Когда объект размещен в памяти так, как это изображено выше, смещение, хранимое с указателем виртуальной функции, исчисляется вычитанием смещения класса, для которого эта таблица vtbl создана, из смещения класса, поставляющего эту функцию. Рассмотрим пример:
void callvirt(w*pw) { pw ! f(); } main () { callvirt(new BMW); }
В функции main вызов callvirt с указателем на BMW требует приведения к указателю на W, поскольку функция callvirt ожидает параметр типа W*. Так как функция callvirt вызывает f() (через указатель на BMW, преобразованный к указателю на W ), будет использована таблица vtbl класса W (в BMW ), где указано, что экземпляром виртуальной функции f(), которую нужно вызвать, является BW :: f(). Чтобы передать функции BW :: f() указатель this на BW, указатель pw должен быть вновь приведен к указателю на BMW (вычитанием смещения для W ), а затем к указателю на BW (добавлением смещения BW в объекте BMW ). Значение смещения BW в объекте BMW минус смещение W в объекте BMW и есть смещение, хранимое в строке таблицы vtbl для w в BMW для функции BW :: f().