Что такое pure virtual call (вызов чистой виртуальной функции) в C++?

«Что такое pure virtual call (вызов чистой виртуальной функции) в C++?» — вопрос из категории ООП, который задают на 25% собеседований C/C++ Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Pure virtual call (вызов чистой виртуальной функции) — это ошибка времени выполнения в C++, которая возникает при попытке вызвать чистую виртуальную функцию через указатель или ссылку на базовый класс, когда для фактического объекта производного класса эта функция не была переопределена (или объект базового класса не существует). Это приводит к неопределённому поведению (Undefined Behavior, UB), чаще всего — к аварийному завершению программы.

Механизм: Во время выполнения вызов виртуальной функции осуществляется через таблицу виртуальных функций (vtable). Если функция объявлена как чистая виртуальная (= 0), её запись в vtable базового класса обычно содержит указатель на специальную функцию-заглушку (например, __cxa_pure_virtual), которая и вызывает аварийное завершение.

Типичные причины возникновения:

  1. Вызов виртуальной функции в конструкторе или деструкторе базового класса. Внутри конструктора базового класса объект производного класса ещё не сконструирован (или уже разрушен в деструкторе), поэтому механизм виртуальных функций работает некорректно — вызывается версия функции из базового класса, которая является чистой виртуальной.
  2. Удаление объекта через указатель на базовый класс, у которого нет виртуального деструктора. Это может привести к неполному разрушению объекта и последующему обращению к частично уничтоженной vtable.
  3. Обращение к объекту после его уничтожения (use-after-free). Указатель "висящий" (dangling pointer) может быть использован для вызова виртуальной функции.
  4. Неправильная инициализация указателя или ссылки, когда они указывают на несуществующий объект базового класса.

Пример опасного кода:

#include <iostream>

class AbstractBase {
public:
    virtual void doWork() = 0; // Чистая виртуальная функция
    virtual ~AbstractBase() = default;

    AbstractBase() {
        // КРИТИЧЕСКАЯ ОШИБКА: вызов чистой виртуальной функции в конструкторе!
        // На момент выполнения конструктора AbstractBase объект производного
        // класса ещё не существует. Компилятор может предупредить об этом.
        doWork(); // Pure virtual call! Неопределённое поведение.
    }
};

class ConcreteDerived : public AbstractBase {
public:
    void doWork() override {
        std::cout << "ConcreteDerived::doWork() called.n";
    }
};

int main() {
    // При попытке создания объекта произойдёт pure virtual call.
    // Программа, скорее всего, аварийно завершится.
    ConcreteDerived obj; // ОПАСНО!
    return 0;
}

Как избежать:

  • Никогда не вызывайте виртуальные функции (особенно чистые) из конструкторов и деструкторов базового класса. Если нужна общая инициализация, вынесите её в отдельный невиртуальный метод (например, init()), который будет явно вызван после полного создания объекта.
  • Для полиморфных базовых классов всегда объявляйте виртуальный деструктор. Это гарантирует корректное разрушение объектов производных классов.
  • Внимательно управляйте временем жизни объектов. Избегайте висячих указателей и ссылок.
  • Используйте умные указатели (std::unique_ptr, std::shared_ptr) для автоматического управления временем жизни.