Виртуальные функции. Полиморфизм. Цель. синтаксис, примеры использования

ВИРТУАЛЬНЫХ ФУНКЦИЙ (virtual functions). Такая особая форма функций,

будучи введенной в базовом классе и помеченной ключевым словом virtual, может быть замещена, или переопределена (overridden), другой реализацией с идентичной сигнатурой в производном классе. А в момент вызова виртуальной функции через указатель или ссылку на базовый класс будет использоваться реализация, соответствующая фактическому типу объекта.

 

ПОЛИМОРФНЫМ (polymorphism = много форм). Виртуальные функции функционируют таким образом, что при вызове через указатель или ссылку на базовый класс управление попадает в версию функции, соответствующую типу вызываемого объекта, известному в момент его создания. Полиморфизм является одним из трех фундаментальных принципов в объектно-ориентированном программировании,

 

Отметим преимущества использования такого подхода:

● основной реализуемый алгоритм не заботится ни о преобразовании типа вниз по иерархии, ни о полноте рассмотрения всех возможных объектов - используется единый интерфейс, предполагая, что объект обеспечит корректное поведение, соответствующее своему классу;

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

 

classVehicle

{

public:

virtual floatgetMaxExtraWeight () const { return 0.0f; }

};

 

 

Реализация виртуальных функций. Указатель VPTR и таблица VTABLE. Вызов виртуальной функции. Инициализация служебных данных для работы виртуальных функций в конструкторах.

В начало каждого объекта полиморфного класса помещается неявный указатель VPTR (virtual functions table pointer) на соответствующую таблицу виртуальных функций. При чем для всех объектов одного и того же конкретного класса этот указатель указывает на одну и ту же таблицу виртуальных функций. Например, в начале двух объектов-файлов будут одинаковые указатели VPTR на одну и ту же таблицу VTABLE для класса File:

 

В свою очередь, два объекта-директории будут иметь аналогичные указатели VPTR на таблицу виртуальных функций своего класса Directory:

 

Фактическое значение указателя VPTR неявно устанавливается в конструкторе

объекта. Конструктор каждого класса самостоятельно отвечает за установку значения данного указателя на нужную таблицу. В момент, пока управление не вышло из конструктора базового класса в конструктор производного, значение указателя vptr пока не соответствует таблице конкретного класса, а используется таблица базового класса.

Как только управление переходит в конструктор дочернего класса, указатель переключается на другую таблицу:

 

- фрагмент дизассемблированного кода конструктора базового класса:

; Установка указателя vptr на таблицу виртуальных функций класса FilesystemEntry

008B1225 mov dword ptr [eax],offset FilesystemEntry::`vftable' (8C6338h)

 

- фрагмент дизассемблированного кода конструктора производного класса:

; Вызов конструктора базового класса

00054689 call FilesystemEntry::FilesystemEntry (516D1h)

0005468E mov dword ptr [ebp-4],0

00054695 mov eax,dword ptr [ebp-14h]

; Установка указателя vptr на таблицу виртуальных функций класса Directory

00054698 mov dword ptr [eax],offset Directory::`vftable' (74978h)

 

pRootDir->show( std::cout, 0 );

; Передача аргументов для функции (заталкиваются в сегмент стека в обратном порядке)

00A0C999 push 0

00A0C99B mov eax,dword ptr [__imp_std::cout (0A1D364h)]

00A0C9A0 push eax

; В регистр ecx записывается адрес объекта - указателя на базовый класс (pRootDir)

00A0C9A1 mov ecx,dword ptr [ebp-18h]

; В регистр edx из объекта извлекается адрес таблицы виртуальных функций

00A0C9A4 mov edx,dword ptr [ecx]

; В регистр eax записывается адрес функции со смещением от начала таблицы на 8 байт.

; На 32-битной платформе это соответствует строчке таблицы с порядковым номером №2

00A0C9A9 mov eax,dword ptr [edx+8]

; Осуществляется вызов функции через извлеченный адрес

00A0C9AC call eax

 

 

Контроль переопределения виртуальных функций. Требования к сигнатурам. Ключевые слова override и final. Ковариантность возвращаемых типов.

Переопределение виртуальных функций из базовых классов в производных осуществляется на основе сопоставления сигнатур:

● названия виртуального метода;

● количества и типов аргументов;

● модификатора const на методе (влияет на неявный указатель this);

● возвращаемого типа.

 

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

 

Очевидно, если не совпадают названия, нет никакой возможности сопоставить функции.

 

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

Такие ошибки чаще возникают не сразу, а при последующей модификации кода, когда в базовый класс вносится изменение в сигнатурах виртуальных методов, а в производном сделать аналогичные изменения забывают. Во избежание подобных ошибок, в новом стандарте языка С++’11 добавлено специальное ключевое слово override для выражения более явного намерения переопределить виртуальную функцию из базового класса. Встретив такое ключевое слово, компилятор проверяет действительно ли в базовых классах существует виртуальная функция с совместимой сигнатурой. Если такая функция фактически не обнаруживается, компилятор генерирует ошибку.

 

Кроме того, в С++’11 было также предложено ключевое слово final, запрещающее дальнейшее переопределение виртуальных функций в производных классах. Такое поведение может потребоваться авторам библиотек, которые считают разумным запретить дальнейшее наследование и переопределение виртуальных методов иерархии как нелогичное:

Ковариантность (covariance) - когда возвращаемый тип в переопределяемом методе производного класса является производным от возвращаемого типа в методе базового класса. Например, для иерархии объектов файловой системы можно представить некоторую виртуальную функцию для “клонирования”, создающую точную копию объекта, имея указатель или ссылку на базовый класс. Должна создаваться полная копия, учитывая возможные дочерние элементы директорий

Такой метод может быть переопределен с ковариантным возвращаемым типом .

За счет ковариантного возвращаемого типа стало возможным получение более точного объекта-результата без необходимости небезопасного преобразования типа вниз по иерархии.