tgoop.com/cxx95/67
Create:
Last Update:
Last Update:
#creepy
Миф о виртуальных деструкторах
На собеседованиях и в реальной жизни часто встречается вопрос: "Зачем нужен виртуальный деструктор?"
В очень достоверном источнике знаний (то есть в интернете) практически везде написано в таком ключе:
Если у вас в классе присутствует хотя бы один виртуальный метод, деструктор также следует сделать виртуальным. При этом не следует забывать, что деструктор по умолчанию виртуальным не будет, поэтому следует объявить его явно. Если этого не сделать, у вас в программе почти наверняка будут UB (скорее всего в виде утечек памяти).Но есть логичный вопрос: почему бы тогда компилятору не генерировать виртуальный деструктор автоматически (для классов с виртуальными методами)?
Ведь он кучу всего генерирует сам (например move- и copy-конструкторы, move- и copy-операторы присваивания).
Ответ: Потому что утверждение выше неправильное!
Пусть есть базовый виртуальный класс
DrinkMachine
и его наследник класс CoffeeMachine
.Каждый объект программы рано или поздно надо разрушить, это делается в 2х разных случаях:
{В этом случае неважно какой деструктор (виртуальный или нет), потому что в обоих случаях вызовется то что нужно - деструктор реального объекта.
CoffeeMachine cm;
// перед выходом из scope сам вызывается cm.~CoffeeMachine()
}
DrinkMachine* dm = ...; // возможно вместо `...` здесь был `new CoffeeMachine()`(или если у нас объект по типу
// ...
delete dm;
std::unique_ptr<DrinkMachine>
- происходит то же самое)Оператор
delete
обычно делает две вещи: вызывает деструктор и освобождает память:dm->~DrinkMachine();В этом случае важно чтобы вызвался именно деструктор реального объекта, т.е. возможно мы на самом деле хотели бы вызвать
std::free(dm);
~CoffeeMachine()
. В этом случае нужен виртуальный деструктор - он будет лежать во vtable, и как метод будет находиться динамически.Если второго случая в программе не бывает, то виртуальный деструктор не нужен - например в этой программе все работает без ошибок:
void MakeDrink(DrinkMachine& dm) {
dm.MakeDrink(); // вызов виртуального метода
}
void MakeCoffee() {
CoffeeMachine cm;
MakeDrink(cm);
// cm удалится сам через `cm.~CoffeeMachine()`
}
Это может быть важно для программ, которых нужно оптимизировать, потому что вызов виртуального метода (в т.ч. виртуального деструктора) дает оверхед в виде двух memory load.P. S. Есть такой прикол как девиртуализация - когда компилятор сразу понимает какой метод нужно вызвать (не глядя во vtable). Но для этого компилятор должен доказать, что он точно знает, какой метод нужно вызвать. Хорошая статья на эту тему.
Девиртуализация не стандартизирована, поэтому нужно проверять на своем компиляторе самому - оптимизируется ли вызов виртуального деструктора в примере с
MakeCoffee
выше или нет. Если да - то можно забить и всегда делать виртуальный деструктор.