Найти в Дзене
ZDG

От C к C++: Виртуальные функции

Предыдущая часть:

Рассмотрим для затравки пример из другого языка – PHP:

Несмотря на разницу в синтаксисе, легко понять, что класс Child наследуется от класса Prnt. Я его назвал Prnt, так как Parent в PHP это зарезервированное слово. И родитель, и потомок имеют метод setX().

Далее, мы передаём в функцию test() экземпляры классов Prnt и Child и смотрим, что происходит.

$parent = new Prnt();
test($parent);
$child = new Child();
test($child);

Когда передаём $parent, то вызывается метод Prnt::setX(), когда передаём $child, вызывается Child::setX(), то есть это два разных метода, и какой из них вызывать, определяет класс переданного объекта.

Функция ожидает на входе конкретный класс Prnt, но почему она тогда принимает класс Child? Потому что он потомок Prnt и значит включает в себя Prnt. Хорошо, а почему тогда вызывается не метод Prnt::setX(), а метод Child::setX()? Ведь раз функция ожидает Prnt, то и работать она должна с методами Prnt?

Во-о-от, а теперь посмотрим на C++:

-2

В таком варианте, хотя мы и будем передавать в функцию test() экземпляры Parent и Child, будет всегда вызываться метод Parent::setX(). Что вроде бы и логично, так как мы явно указали, что аргумент функции имеет тип Parent.

Сама функция test() генерируется таким образом, что в ней жёстко прописывается вызов Parent::setX(). А строки "setX child" в программе нет вообще, потому что она никогда не используется.

В чём разница между PHP и C++?

PHP в некоторых аспектах стал типизированным языком, но при этом сохраняет динамические типы, которые определяются не во время компиляции, а во время исполнения. Функция test() не знает, есть ли в переданном объекте метод setX(), пока не поищет его. Это значит, что объект должен приносить с собой свои методы. Соответственно, Prnt приносит свой setX(), а Child свой.

К примеру, если у Child будет свой метод setY(), которого нет у Parent, и мы попытаемся вызвать этот метод в функции test(), то C++ нас немедленно пошлёт. Компилятор знает, что тип аргумента Parent, а у Parent такого метода нет. А PHP просто поищет метод в объекте, и если найдёт, то спокойно его вызовет. А если не найдёт, то программа упадёт.

Мы можем заставить объекты C++ также носить свои методы с собой. Для этого метод setX() в родительском классе надо объявить как виртуальный:

-3

Это приводит к значительным изменениям в сгенерированном коде. У классов Parent и Child вдруг появляются громоздкие конструкторы, а впридачу к ним появляются так называемые

Виртуальные таблицы

Добавление слова virtual к объявлению метода говорит о том, что он теперь будет динамически определяться во время исполнения программы. Как это происходит?

Компилятор заранее строит таблицы методов для каждого класса. У класса Parent появляется своя таблица, где содержится указатель на метод Parent::setX(). У класса Child также появляется своя таблица, где содержится указатель на метод Child::setX().

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

Аналогично создаётся объект класса Child. Его конструктор берёт указатель на таблицу методов для Child и помещает его внутри объекта.

Таким образом, таблица методов назначается именно в момент создания объекта, и затем остаётся с ним всё время. Созданный объект будет носить с собой методы из своей таблицы.

Однако, если повторить эксперимент с передачей в test() объектов разных классов:

Parent parent;
test(parent);
Child child;
test(child);

Мы увидим, что да, объекты конструируются как надо и передаются в функцию, но сама функция по-прежнему использует жёстко прошитый вызов Parent::setX() и поэтому ничего не меняется.

Изменим передачу объектов на указатели:

-4

И на этот раз функция будет вызывать уже не Parent::setX(), а брать из переданного объекта указатель на таблицу, который он принёс с собой, и из этой таблицы брать указатель на метод. Так что при передаче parent будет вызван Parent::setX(), а при передаче child будет вызван Child::setX(). Как в первом примере с PHP.

Почему виртуальные функции работают только с указателями на объекты? Попробуем узнать в руководстве по C++:

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

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

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

Parent parent;
test(parent);
Child child;
test(child);

Когда мы в функцию передаём объект не по указателю, а по значению, то этот объект копируется. В функцию передаётся не он, а его копия. А что происходит при копировании? Вызывается имплицитный конструктор копирования Parent, о котором речь шла в предыдущей части.

Компилятор видит, что мы хотим передать структуру типа Child как аргумент типа Pаrent, и поэтому на лету создаёт из структуры Child временную структуру Parent, и передаёт уже её. Таким образом, функция получает то, что и ожидает – Parent. Значит, тип во время компиляции определён и можно использовать фиксированный вызов Parent::setX(), а не виртуальный.

Имплицитное копирование можно запретить, сделав явный конструктор explicit Parent(Parent const&), но тогда сломается и передача в функцию по значению. Раз нет копирования, нет и передачи.

Думаю, что если тщательно руками написать конструктор копирования из Child в Parent, то наверное можно будет и как-то настроить виртуальные методы, чтобы они вызывались именно от Child. Но это не точно.

Чувствую, что имплицитное копирование ещё много где подпортит жизнь.

Читайте дальше:

Наука
7 млн интересуются