Частично уничтоженные объекты динамически не полиморфны

от автора

Недавно столкнулся с явлением, которое, хотя и хорошо объясняется, но, по крайней мере для меня, не тривиально. Сразу приношу извинения тем, для кого приведенные далее вещи очевидны. Возьмем код:

#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 (то есть в стандарте замена таблицы не требуется) или «тёмным углом стандарта» (если этот стандарт требует замены таблицы) выводы напрашиваются одни и те же:

  1. Частично уничтоженный объект, то есть объект во время вызова деструктора одного из родителей, не может быть полиморфным
  2. Нужно избегать вызова собственных виртуальных функций из деструктора. Причем это касается в том числе опосредованных вызовов, то есть приведенный код обладает тем же самым поведением:
    class Base2 { public: 	virtual ~Base2(); 	void helloFromClass() const { 		helloFromClassCall(); 	} protected: 	virtual void helloFromClassCall() const; };  Base2::~Base2() { 	helloFromClass(); //опосредованный вызов виртуального helloFromClassCall() } 

  3. Из деструктора можно вызвать и чистые виртуальные функции, что другими способами сделать достаточно не просто:
    class Base3 { public: 	virtual ~Base3(); protected: 	virtual void helloFromClass() const = 0; };  Base3::~Base3() { 	helloFromClass(); //вызов чистой виртуальной функции: не в деструкторе не произошел бы } 

  4. Скорее всего это уже решено, думаю, что компиляторы или хотя бы статические анализаторы должны предупреждать о вызовах виртуальных функций из деструкторов, тем более чистых виртуальных функций.
  5. У проблемы есть и обратная сторона: вызов виртуальных функций из конструкторов, то есть отсутствие динамического полиморфизма частично сконструированных объектов, но на практике такое поведение я не проверял.

ссылка на оригинал статьи http://habrahabr.ru/post/165685/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *