Какие особенности вызова виртуальной функции из деструктора в C++?

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

Ответ

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

Причина: Порядок построения/разрушения объектов в C++ строго определен. При вызове деструктора базового класса производная часть объекта уже считается разрушенной. Вызов переопределенного метода из производного класса в этот момент привел бы к неопределенному поведению, так как члены производного класса могли быть уже уничтожены или находиться в невалидном состоянии.

Пример:

#include <iostream>

class Base {
public:
    virtual void log() { std::cout << "Base::log()n"; }
    virtual ~Base() {
        log(); // Всегда вызовет Base::log(), даже для объекта Derived
    }
};

class Derived : public Base {
public:
    void log() override { std::cout << "Derived::log()n"; }
    ~Derived() {
        // Деструктор Derived уже завершил работу к моменту вызова ~Base()
    }
};

int main() {
    Derived d; // При разрушении выведет "Base::log()"
    return 0;
}

Практическое следствие: Не стоит полагаться на полиморфное поведение внутри деструкторов и конструкторов. Если нужна общая логика очистки, лучше вынести ее в отдельный невиртуальный protected-метод и явно вызывать его из деструкторов производных классов.