Недавно столкнулся с явлением, которое, хотя и хорошо объясняется, но, по крайней мере для меня, не тривиально. Сразу приношу извинения тем, для кого приведенные далее вещи очевидны. Возьмем код:
#include <iostream>
class Base: {
public:
virtual ~Base();
protected:
virtual void helloFromClass() const;
};
class Derived: public Base {
protected:
void helloFromClass() const;
};
Base::~Base() {
helloFromClass();
}
void Base::helloFromClass() const {
std::cout << "Hello from Basen";
}
void Derived::helloFromClass const {
std::cout << "Hello from Derivedn";
}
Derived d;
int main() {
return 0;
}
Я ожидал, что из-за динамического полиморфизма класса Base программа напишет «Hello from Derived» во время вызова деструктора Base. Сразу хочу сказать, что виртуальность деструктора в данном примере не играет никакого значения, речь далее пойдет о виртуальности helloFromClass(). Те, кто также как и я поначалу, не верят глазам своим, могут скомпилировать и запустить программу: видим: «Hello from Base».
Вспоминаем, что согласно правилам C++, при уничтожении объекта последовательно вызываются деструкторы ~Derived(), потом ~Base(), первый создан компиляторм по умолчанию и не производит никакого вывода. Почему же ~Base() вызывает Base::helloFromClass(), а не Derived::helloFromClass()?
Во-первых, объект d «состоит из двух частей»: Base и Derived. На момент вызова ~Base() «половинка Derived» объекта d уже уничтожена, а сам d частично уничтожен. Поэтому вызов Derived::helloFromClass() вообще не корректен, поскольку произошел бы для несуществующего объекта — «половинки Derived» объекта d. Например, если бы Derived::helloFromClass() обращался к данным Derived, то это было бы вообще обращение к деинициализированным данным, например, к памяти занятой уже другим объектом или не отображенной в пространство процесса. То есть, undefined behavior во всей красе.
Во-вторых, очевидно, вызов Base::helloFromClass() произошел потому, что указатель на таблицу виртуальных методов d был изменен. То есть, порядок действий таков: вызов d.~Derived(), замена таблицы виртуальных методов d, вызов d.~Base(). Лень проверять, поэтому не знаю, является такое поведение стандартным или реализовано конкретным компилятором (в моем случае gcc4).
Независимо от того, является приведенный код undefined behavior (то есть в стандарте замена таблицы не требуется) или «тёмным углом стандарта» (если этот стандарт требует замены таблицы) выводы напрашиваются одни и те же:
- Частично уничтоженный объект, то есть объект во время вызова деструктора одного из родителей, не может быть полиморфным
- Нужно избегать вызова собственных виртуальных функций из деструктора. Причем это касается в том числе опосредованных вызовов, то есть приведенный код обладает тем же самым поведением:
class Base2 { public: virtual ~Base2(); void helloFromClass() const { helloFromClassCall(); } protected: virtual void helloFromClassCall() const; }; Base2::~Base2() { helloFromClass(); //опосредованный вызов виртуального helloFromClassCall() }
- Из деструктора можно вызвать и чистые виртуальные функции, что другими способами сделать достаточно не просто:
class Base3 { public: virtual ~Base3(); protected: virtual void helloFromClass() const = 0; }; Base3::~Base3() { helloFromClass(); //вызов чистой виртуальной функции: не в деструкторе не произошел бы }
- Скорее всего это уже решено, думаю, что компиляторы или хотя бы статические анализаторы должны предупреждать о вызовах виртуальных функций из деструкторов, тем более чистых виртуальных функций.
- У проблемы есть и обратная сторона: вызов виртуальных функций из конструкторов, то есть отсутствие динамического полиморфизма частично сконструированных объектов, но на практике такое поведение я не проверял.
Автор: oleg1977