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

в 0:45, , рубрики: c++, virtual, С++, метки: ,

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

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

  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. У проблемы есть и обратная сторона: вызов виртуальных функций из конструкторов, то есть отсутствие динамического полиморфизма частично сконструированных объектов, но на практике такое поведение я не проверял.

Автор: oleg1977

Источник

Поделиться

* - обязательные к заполнению поля