ProgrammazioneC++ разработчик

Что такое virtual table (vtable) в C++? Как компилятор реализует динамический полиморфизм, и какие тонкости связаны с этим механизмом?

Supera i colloqui con l'assistente IA Hintsage

Ответ.

В C++ динамический полиморфизм реализуется через механизм виртуальных функций с помощью специальной таблицы — vtable (virtual table). Для любого класса с хотя бы одной виртуальной функцией компилятор генерирует vtable: это массив указателей на виртуальные функции класса. Каждый объект такого класса хранит скрытый указатель на vtable — так называемый vptr (virtual pointer).

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

Пример:

class Base { public: virtual void foo() { std::cout << "Base::foo() "; } }; class Derived : public Base { public: void foo() override { std::cout << "Derived::foo() "; } }; void call_foo(Base* obj) { obj->foo(); // вызов через vtable }

Вызов foo будет обращением к vtable: если в Derived, то вызовется переопределённая функция.

Тонкости:

  • Если класс не содержит виртуальных функций, vtable и vptr не добавляются.
  • Виртуальные методы не становятся inlined, зачастую теряя производительность.
  • Добавляется размер экземпляра класса (на размер vptr — обычно это 4/8 байт).
  • Виртуальные вызовы медленнее прямых вызовов.

Вопрос с подвохом.

"Является ли метод виртуальным, если он объявлен как virtual, но не переопределён в производном классе? Будет ли в этом случае работать вызов через vtable?"

Ответ: Да, если функция объявлена как virtual в базовом классе, она остаётся виртуальной для всех потомков, даже если не переопределена. Вызов через базовый указатель в любом случае пойдёт через vtable. Если метод не переопределён — вызовется версия из базового класса.

Пример:

struct Base { virtual void foo() { std::cout << "B "; } }; struct Derived : Base { }; Base* obj = new Derived(); obj->foo(); // Выведет "B", но вызов через vtable!

Примеры реальных ошибок из-за незнания тонкостей темы.


История

На большом проекте забыли объявить деструктор как виртуальный в базовом классе с виртуальными функциями. Это привело к утечкам памяти — при удалении по базовому указателю вызывался только деструктор базового класса, не потомка.


История

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


История

Актуализация vtable после множественного наследования привела к ошибкам: неверные вызовы методов при использовании dynamic_cast, т.к. не учитывалось смещение vptr в сложной иерархии классов.