#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 Base\n"; } void Derived::helloFromClass const { std::cout << "Hello from Derived\n"; } 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(); //вызов чистой виртуальной функции: не в деструкторе не произошел бы }
- Скорее всего это уже решено, думаю, что компиляторы или хотя бы статические анализаторы должны предупреждать о вызовах виртуальных функций из деструкторов, тем более чистых виртуальных функций.
- У проблемы есть и обратная сторона: вызов виртуальных функций из конструкторов, то есть отсутствие динамического полиморфизма частично сконструированных объектов, но на практике такое поведение я не проверял.
ссылка на оригинал статьи http://habrahabr.ru/post/165685/
Добавить комментарий