Есть ли веская причина не объявлять виртуальный деструктор для класса? В каких случаях лучше не писать?
Когда не следует использовать виртуальные деструкторы?
Ответы (12)
Нет необходимости использовать виртуальный деструктор, если верно любое из следующих утверждений:
- Нет намерения выводить из него классы
- Нет экземпляра в куче
- Нет намерения хранить указатель суперкласса
Нет особой причины избегать этого, если только у вас действительно не хватает памяти.
Base *b = new Derived(); delete b;
см.
- person Frank Sebastià; 03.01.2018
Чтобы ответить на вопрос явно, т.е. когда вам не объявлять виртуальный деструктор.
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, чтобы избежать накладных расходов на функции, потому что эти классы специально используются во внутреннем цикле многих высокопроизводительных приложений.
Операция, которая будет выполняться над базовым классом и которая должна вести себя виртуально, должна быть виртуальной. Если удаление может быть выполнено полиморфно через интерфейс базового класса, тогда оно должно вести себя виртуально и быть виртуальным.
Деструктору не обязательно быть виртуальным, если вы не собираетесь наследовать от класса. И даже если вы это сделаете, защищенный невиртуальный деструктор будет столь же хорош, если удаление указателей базового класса не требуется.
Ответ о производительности - единственный, который я знаю, и который имеет шанс быть правдой. Если вы измерили и обнаружили, что де-виртуализация ваших деструкторов действительно ускоряет процесс, то у вас, вероятно, есть другие вещи в этом классе, которые тоже нуждаются в ускорении, но на этом этапе есть более важные соображения. Когда-нибудь кто-нибудь обнаружит, что ваш код предоставит им хороший базовый класс и сэкономит им неделю работы. Вам лучше убедиться, что они сделают работу на этой неделе, скопировав и вставив ваш код, вместо того, чтобы использовать ваш код в качестве основы. Вам лучше сделать некоторые из ваших важных методов закрытыми, чтобы никто не мог унаследовать их от вас.