Зачем нужна таблица виртуальных методов в C++
Что проверяют
Понимаете ли вы, во что компилятор превращает virtual, и можете ли вывести из этого практические следствия: стоимость вызова, поведение в конструкторе, нужность виртуального деструктора. Следом часто просят нарисовать память объекта или объяснить, что меняет множественное наследование.
Как отвечать
Таблица виртуальных методов нужна для динамической диспетчеризации: вызов shape->draw() должен попасть в реализацию реального типа объекта, который известен только во время выполнения. Стандарт C++ vtable не упоминает, это способ реализации, но его используют все основные компиляторы: GCC и Clang по Itanium C++ ABI, MSVC по своему ABI.
Как это устроено:
- одна таблица на класс: массив адресов виртуальных функций для каждого полиморфного класса. Наследник получает слоты базы и подменяет те, что переопределил;
- vptr в каждом объекте: скрытый указатель, обычно в начале объекта, 8 байт на 64-битной платформе;
- вызов: прочитать vptr, взять адрес из нужного слота, сделать косвенный вызов;
- метаданные: в таблице лежит и указатель на
type_info, на нём работаютtypeidиdynamic_cast.
При множественном наследовании у объекта несколько vptr, по одному на полиморфную базу, и при приведении к базе указатель сдвигается.
Цена не столько в косвенном переходе, сколько в том, что компилятор не может встроить функцию. Помогают final и LTO: если тип известен, происходит девиртуализация. Когда полиморфизм нужен без накладных расходов, есть CRTP и std::variant с std::visit.
Где спотыкаются
Забывают виртуальный деструктор. delete наследника через указатель на базу без virtual ~Base() это неопределённое поведение, на практике деструктор наследника не вызовется.
Вызывают виртуальную функцию из конструктора и ждут версию наследника. Пока работает конструктор базы, vptr указывает на таблицу базы, поэтому вызовется её версия, а вызов чистой виртуальной функции закончится аварийным завершением программы.
Говорят, что vtable есть у каждого объекта. Таблица одна на класс, в объекте только указатель, а у класса без виртуальных функций нет ни того, ни другого.