Виртуальный деструктор



В предыдущей статье в комментарии к примеру было написано, что деструктор полиморфного класса обязательно должен быть виртуальным. Зачем же? Погнали разбираться!



Объект каждого класса обладает своим жизненным циклом: начало, счастливая жизнь и конец. С началом ассоциирован - конструктор, а с концом - деструктор, о котором и пойдет речь дальше. Он вызывается автоматически, когда объект удаляется вручную или автоматически. Это даёт возможность корректно завершить работу, которую выполнял объект. Например, освободить выделенную в конструкторе память, закрыть сокет. Это позволяет поддерживать систему в валидном состоянии на протяжении всей работы программы. Это счастливый конец!



Условно, если вы поработали в мастерской, значит, уходя из неё, надо всё за собой убрать и разложить по полочкам. Иначе следующий мастер там просто не найдет нужный инструмент, запнется о мусор и еще что-нибудь испортит. Повторим это с десяток раз и можно сжигать мастерскую 😃. Кажется, что сжигать мастерскую — это перебор 🤭, но именно так и поступит система: прибьет её из-за исчерпания памяти (Out Of Memory). Это грустный конец...



Вернёмся к динамическому полиморфизму. Давайте свяжем два наблюдения:

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

2) Зачастую, взаимодействие происходит через указатель на родительский класс.



Из П.1 следует, что у наследника должен быть вызван деструктор, в котором происходит возврат ресурса системе.

Из П.2 следует, что динамический тип объекта может отличаться от типа указателя.



Из этого следует, что корректное удаление объекта подразумевает вызов деструктора класса наследника. И вот как его вызвать, если тип указателя - родительский? Например тут:

Parent *data = new Child();

...

delete data;



Пишу тут new и delete в ознакомительных целях. Используйте умные указатели: unique_ptr, shared_ptr.



Есть простое встроенное решение 😊 Отмечайте деструктор родительского класса виртуальным! Пример:

struct Parent

{

...

   virtual ~Parent() {...}

   ...

};



Вызов виртуального деструктора приведёт к вызову цепочки деструкторов у всех наследников от родительского до динамического типа:

... -> ~Child_2() -> ~Child_1() -> ~Parent();



Если в иерархии классов деструктор не был отмечен виртуальным, то будет вызван только деструктор класса, который является типом указателя. Деструктор наследника не будет вызван вообще, следовательно, ресурс будет упущен. Добавлю живой пример, в котором количество выделяемых ресурсов не совпадает с освобождаемым. Как-нибудь напишем, как это дело проверять по-нормальному 😉



Пока деструктор останется невиртуальным, у компилятора просто нет указания, что надо заботиться о чем-то. Ну вдруг у вас умышленно очень хитрое поведение у программы?



Есть еще один тонкий момент. Кажется, что если полиморфное семейство не выделяет никаких ресурсов, то и проблем не будет. Но это пока! Пройдет время, код эволюционирует, появятся такие ресурсы и вот тогда что-то может да потечь. Короче, это чеховское ружьё 😉



Помочь избежать этих проблем поможет, как всегда, предупреждение:

-Wdelete-non-virtual-dtor



#cppcore