Есть ли веская причина не объявлять виртуальный деструктор для класса? Когда лучше не писать?
Есть ли веская причина не объявлять виртуальный деструктор для класса? Когда лучше не писать?
Ответы:
Нет необходимости использовать виртуальный деструктор, если верно любое из следующих утверждений:
Нет особой причины избегать этого, если только у вас действительно не хватает памяти.
Чтобы ответить на этот вопрос в явном виде, то есть , когда вы должны не объявить виртуальный деструктор.
C ++ '98 / '03
Добавление виртуального деструктора может изменить ваш класс с POD (простые старые данные) * или агрегированного на не-POD. Это может помешать компиляции вашего проекта, если ваш тип класса где-то агрегатно инициализирован.
struct A {
// virtual ~A ();
int i;
int j;
};
void foo () {
A a = { 0, 1 }; // Will fail if virtual dtor declared
}
В крайнем случае такое изменение может также вызвать неопределенное поведение, когда класс используется способом, требующим POD, например, передача его через параметр многоточия или использование с memcpy.
void bar (...);
void foo (A & a) {
bar (a); // Undefined behavior if virtual dtor declared
}
[* Тип POD - это тип, который имеет определенные гарантии в отношении своей структуры памяти. Стандарт действительно только говорит, что если вы скопируете из объекта с типом POD в массив символов (или беззнаковых символов) и обратно, то результат будет таким же, как у исходного объекта.]
Современный C ++
В последних версиях C ++ концепция POD была разделена между компоновкой класса и его построением, копированием и уничтожением.
Для случая многоточия это больше не неопределенное поведение, теперь оно условно поддерживается семантикой, определяемой реализацией (N3937 - ~ C ++ '14 - 5.2.2 / 7):
... Передача потенциально оцениваемого аргумента типа класса (пункт 9), имеющего нетривиальный конструктор копирования, нетривиальный конструктор перемещения или нетривиальный деструктор без соответствующего параметра, условно поддерживается реализацией - определенная семантика.
Объявление деструктора кроме =default
означает, что это нетривиально (12.4 / 5)
... Деструктор тривиален, если он не предоставлен пользователем ...
Другие изменения в Modern C ++ уменьшают влияние проблемы агрегированной инициализации, поскольку можно добавить конструктор:
struct A {
A(int i, int j);
virtual ~A ();
int i;
int j;
};
void foo () {
A a = { 0, 1 }; // OK
}
Я объявляю виртуальный деструктор тогда и только тогда, когда у меня есть виртуальные методы. Когда у меня есть виртуальные методы, я не верю себе, чтобы избежать создания их экземпляров в куче или сохранения указателя на базовый класс. Обе эти операции являются чрезвычайно распространенными и часто вызывают утечку ресурсов незаметно, если деструктор не объявлен виртуальным.
Виртуальный деструктор необходим всякий раз, когда есть шанс delete
вызвать указатель на объект подкласса с типом вашего класса. Это гарантирует, что правильный деструктор вызывается во время выполнения, и компилятору не нужно знать класс объекта в куче во время компиляции. Например, предположим, что B
это подкласс A
:
A *x = new B;
delete x; // ~B() called, even though x has type A*
Если ваш код не критичен к производительности, было бы разумно добавить виртуальный деструктор к каждому базовому классу, который вы пишете, просто для безопасности.
Однако, если вы обнаружили, что delete
в замкнутом цикле много объектов, могут быть заметны издержки производительности при вызове виртуальной функции (даже пустой). Компилятор обычно не может встроить эти вызовы, и процессору может быть сложно предугадать, куда идти. Вряд ли это сильно повлияет на производительность, но об этом стоит упомянуть.
Виртуальные функции означают, что каждый выделенный объект увеличивает стоимость памяти на указатель таблицы виртуальных функций.
Поэтому, если ваша программа включает выделение очень большого количества некоторого объекта, было бы целесообразно избегать всех виртуальных функций, чтобы сэкономить дополнительные 32 бита на каждый объект.
Во всех остальных случаях вы избавите себя от лишних хлопот, сделав dtor виртуальным.
Не все классы C ++ подходят для использования в качестве базового класса с динамическим полиморфизмом.
Если вы хотите, чтобы ваш класс подходил для динамического полиморфизма, его деструктор должен быть виртуальным. Кроме того, любые методы, которые подкласс может захотеть переопределить (что может означать все общедоступные методы, а также потенциально некоторые защищенные, используемые внутри), должны быть виртуальными.
Если ваш класс не подходит для динамического полиморфизма, то деструктор не следует помечать как виртуальный, потому что это вводит в заблуждение. Это просто побуждает людей неправильно использовать ваш класс.
Вот пример класса, который не подходил бы для динамического полиморфизма, даже если бы его деструктор был виртуальным:
class MutexLock {
mutex *mtx_;
public:
explicit MutexLock(mutex *mtx) : mtx_(mtx) { mtx_->lock(); }
~MutexLock() { mtx_->unlock(); }
private:
MutexLock(const MutexLock &rhs);
MutexLock &operator=(const MutexLock &rhs);
};
Весь смысл этого класса в том, чтобы сидеть в стеке для RAII. Если вы передаете указатели на объекты этого класса, не говоря уже о его подклассах, значит, вы делаете это неправильно.
Хорошая причина не объявлять деструктор как виртуальный - это избавить ваш класс от добавления таблицы виртуальных функций, и вам следует избегать этого, когда это возможно.
Я знаю, что многие люди предпочитают просто объявлять деструкторы виртуальными на всякий случай. Но если в вашем классе нет других виртуальных функций, тогда действительно нет смысла иметь виртуальный деструктор. Даже если вы передадите свой класс другим людям, которые затем наследуют от него другие классы, у них не будет причин когда-либо вызывать удаление указателя, который был приведен к вашему классу - и если они это сделают, я буду считать это ошибкой.
Хорошо, есть одно единственное исключение, а именно, если ваш класс (неправильно) используется для выполнения полиморфного удаления производных объектов, но тогда вы - или другие ребята - надеюсь, знаете, что для этого требуется виртуальный деструктор.
Другими словами, если в вашем классе есть не виртуальный деструктор, то это очень четкое утверждение: «Не используйте меня для удаления производных объектов!»
Если у вас очень маленький класс с огромным количеством экземпляров, накладные расходы на указатель vtable могут повлиять на использование памяти вашей программой. Если в вашем классе нет других виртуальных методов, то создание невиртуального деструктора сэкономит накладные расходы.
Я обычно объявляю деструктор виртуальным, но если у вас есть критический для производительности код, который используется во внутреннем цикле, вы можете избежать поиска в виртуальной таблице. В некоторых случаях это может быть важно, например, при проверке столкновений. Но будьте осторожны с тем, как вы уничтожаете эти объекты, если вы используете наследование, иначе вы уничтожите только половину объекта.
Обратите внимание, что поиск в виртуальной таблице выполняется для объекта, если какой-либо метод этого объекта является виртуальным. Таким образом, нет смысла удалять виртуальную спецификацию в деструкторе, если у вас есть другие виртуальные методы в классе.
Если вы абсолютно уверены, что ваш класс не имеет vtable, тогда у вас также не должно быть виртуального деструктора.
Это редкий случай, но бывает.
Наиболее знакомым примером шаблона, который делает это, являются классы DirectX D3DVECTOR и D3DMATRIX. Это методы класса, а не функции для синтаксического сахара, но классы намеренно не имеют vtable, чтобы избежать накладных расходов на функции, поскольку эти классы специально используются во внутреннем цикле многих высокопроизводительных приложений.
Операция, которая будет выполняться над базовым классом и которая должна вести себя виртуально, должна быть виртуальной. Если удаление может быть выполнено полиморфно через интерфейс базового класса, оно должно вести себя виртуально и быть виртуальным.
Деструктору не обязательно быть виртуальным, если вы не собираетесь наследовать от класса. И даже если вы это сделаете, защищенный не виртуальный деструктор будет столь же хорош, если удаление указателей базового класса не требуется .
Ответ о производительности - единственный, который я знаю, и который имеет шанс быть правдой. Если вы измерили и обнаружили, что де-виртуализация ваших деструкторов действительно ускоряет процесс, то у вас, вероятно, есть другие вещи в этом классе, которые тоже нуждаются в ускорении, но на этом этапе есть более важные соображения. Когда-нибудь кто-нибудь обнаружит, что ваш код предоставит им хороший базовый класс и сэкономит им неделю работы. Вам лучше убедиться, что они сделают работу на этой неделе, скопировав и вставив ваш код, вместо того, чтобы использовать ваш код в качестве основы. Вам лучше сделать некоторые из ваших важных методов закрытыми, чтобы никто не мог унаследовать их от вас.