Есть ли когда-нибудь веская причина для объявления нет виртуального деструктора для класса? Когда лучше не писать?





Нет необходимости использовать виртуальный деструктор, если верно любое из следующих утверждений:
Нет особой причины избегать этого, если только у вас действительно не хватает памяти.
Это плохой ответ. «Нет необходимости» отличается от «не следует», а «без намерения» отличается от «сделать невозможным».
Также добавьте: нет намерения удалять экземпляр через указатель базового класса.
Это не совсем ответ на вопрос. Где у вас есть веская причина не использовать виртуальный дтор?
Я считаю, что когда нет необходимости что-то делать, это хороший повод не делать этого. Это следование принципу простого дизайна XP.
Говоря, что у вас «нет намерения», вы делаете огромное предположение о том, как будет использоваться ваш класс. Мне кажется, что самое простое решение в большинстве случаев (которое, следовательно, должно быть по умолчанию) должно заключаться в наличии виртуальных деструкторов и избегать их только в том случае, если у вас есть конкретная причина не делать этого. Так что мне все еще любопытно, что могло бы быть хорошей причиной.
Это не связано точно с кучей, а скорее с областью видимости лексических переменных. Значение любой локальной переменной, когда она является объектом класса, хорошо известно во время компиляции, поэтому не может быть путаницы в том, какой деструктор вызывать. Значения локальных переменных - это именно те значения, которые хранятся в стеке, а не в куче; поэтому только объекты, размещенные в куче, должны быть осторожны с тем, какой деструктор будет вызван.
@Windowsprogrammer: Это правда! Кто из нас совершил ошибку новичка, наследовав от std :: vector? Почти всегда это ужасная идея, но она прекрасно описывает «не следует» и «нельзя / сделать невозможным».
Пожалуйста, поправьте меня, если я ошибаюсь, но говорят, что когда применяется условие Любые, но: Нет экземпляра в куче, вы можете иметь подкласс с динамической памятью, и следующий код приведет к утечке ресурсов: Base *b = new Derived(); delete b;видеть
Размещение @ FrankSebastià с помощью new - это размещение в куче.
@ FrankSebastià Я понимаю, что вы имеете в виду, и следует пояснить второй момент: нет создания экземпляров в куче производных классов.
Я объявляю виртуальный деструктор тогда и только тогда, когда у меня есть виртуальные методы. Когда у меня есть виртуальные методы, я не верю себе, чтобы избежать создания их экземпляров в куче или сохранения указателя на базовый класс. Обе эти операции являются чрезвычайно распространенными и часто приводят к незаметной утечке ресурсов, если деструктор не объявлен виртуальным.
И, действительно, в gcc есть опция предупреждения, которая предупреждает именно об этом случае (виртуальные методы, но не виртуальный dtor).
Разве тогда вы не рискуете потерять память, если вы производите от класса, независимо от того, есть ли у вас другие виртуальные функции?
Я согласен с mag. Это использование виртуального деструктора и / или виртуального метода - это отдельные требования. Виртуальный деструктор предоставляет классу возможность выполнять очистку (например, удалять память, закрывать файлы и т. д.) И также обеспечивает вызов конструкторов всех его членов.
@MagRoader Теоретически да, но поскольку проблема возникает только в том случае, если вы сохраняете (и удаляете) указатель на производный объект, выделенный в куче, в указателе для базы, очевидный вопрос: какое использование этого указателя без виртуальных функций? Я вижу только одну возможность: вы используете объект ТОЛЬКО для удаления ресурсов, когда «готово»; и в этом случае у вас должен быть виртуальный деструктор без каких-либо других методов.
Виртуальный деструктор необходим всякий раз, когда есть вероятность того, что delete может быть вызван по указателю на объект подкласса с типом вашего класса. Это гарантирует, что правильный деструктор вызывается во время выполнения, и компилятору не нужно знать класс объекта в куче во время компиляции. Например, предположим, что B является подклассом A:
A *x = new B;
delete x; // ~B() called, even though x has type A*
Если ваш код не критичен к производительности, было бы разумно добавить виртуальный деструктор к каждому базовому классу, который вы пишете, просто для безопасности.
Однако, если вы обнаружите, что delete загружает множество объектов в плотный цикл, накладные расходы на производительность при вызове виртуальной функции (даже пустой) могут быть заметны. Компилятор обычно не может встроить эти вызовы, и процессору может быть сложно предугадать, куда нужно идти. Вряд ли это сильно повлияет на производительность, но об этом стоит упомянуть.
«Если ваш код не критичен к производительности, было бы разумно добавить виртуальный деструктор к каждому написанному вами базовому классу, просто для безопасности». следует подчеркивать больше в каждом ответе, который я вижу
Ответ о производительности - единственный, который я знаю, и который имеет шанс быть правдой. Если вы измерили и обнаружили, что де-виртуализация ваших деструкторов действительно ускоряет процесс, то у вас, вероятно, есть другие вещи в этом классе, которые тоже нуждаются в ускорении, но на этом этапе есть более важные соображения. Когда-нибудь кто-нибудь обнаружит, что ваш код предоставит им хороший базовый класс и сэкономит им неделю работы. Вам лучше убедиться, что они проделают работу на этой неделе, скопировав и вставив ваш код, вместо того, чтобы использовать ваш код в качестве основы. Вам лучше сделать некоторые из ваших важных методов закрытыми, чтобы никто не мог унаследовать их от вас.
Полиморфизм, безусловно, замедлит работу. Сравните это с ситуацией, когда нам нужен полиморфизм и мы решили не делать этого, он будет еще медленнее. Пример: мы реализуем всю логику в деструкторе базового класса, используя RTTI и оператор switch для очистки ресурсов.
В C++ вы не обязаны мешать мне наследовать ваши классы, которые, как вы задокументировали, не подходят для использования в качестве базовых классов. Я обязуюсь использовать наследование с осторожностью. Если, конечно, руководство по домашнему стилю не говорит иначе.
... просто создание виртуального деструктора не означает, что класс обязательно будет правильно работать как базовый класс. Так что пометить его виртуально «просто потому, что» вместо того, чтобы делать эту оценку, означает выписать чек, который мой код не может обналичить.
Обычно я объявляю деструктор виртуальным, но если у вас есть критический для производительности код, который используется во внутреннем цикле, вы можете избежать поиска в виртуальной таблице. В некоторых случаях это может быть важно, например, при проверке столкновений. Но будьте осторожны с тем, как вы уничтожаете эти объекты, если вы используете наследование, иначе вы уничтожите только половину объекта.
Обратите внимание, что поиск в виртуальной таблице выполняется для объекта, если метод Любые для этого объекта является виртуальным. Так что нет смысла удалять виртуальную спецификацию в деструкторе, если у вас есть другие виртуальные методы в классе.
Чтобы ответить на вопрос явно, т.е. когда вы должны нет объявлять виртуальный деструктор.
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):
...Passing a potentially-evaluated argument of class type (Clause 9) having a non-trivial copy constructor, a non-trivial move constructor, or a on-trivial destructor, with no corresponding parameter, is conditionally-supported with implementation-defined semantics.
Объявление деструктора, отличного от =default, будет означать, что это нетривиально (12.4 / 5)
... A destructor is trivial if it is not user-provided ...
Другие изменения в Modern C++ уменьшают влияние проблемы агрегированной инициализации, поскольку может быть добавлен конструктор:
struct A {
A(int i, int j);
virtual ~A ();
int i;
int j;
};
void foo () {
A a = { 0, 1 }; // OK
}
Вы правы, и я ошибался, производительность - не единственная причина. Но это показывает, что в остальном я был прав: программисту класса лучше включить код, чтобы предотвратить наследование класса кем-либо еще.
дорогой Ричард, не могли бы вы прокомментировать еще немного то, что вы написали. Я не понимаю вашу точку зрения, но это, кажется, единственный ценный момент, который я нашел путем поиска в Google) А может быть вы можете дать ссылку на более подробное объяснение?
@JohnSmith Я обновил ответ. Надеюсь, это поможет.
Виртуальные функции означают, что каждый выделенный объект увеличивает стоимость памяти на указатель таблицы виртуальных функций.
Поэтому, если ваша программа включает выделение очень большого количества некоторого объекта, было бы целесообразно избегать использования всех виртуальных функций, чтобы сэкономить дополнительные 32 бита на каждый объект.
Во всех остальных случаях вы избавите себя от лишних хлопот, сделав dtor виртуальным.
Просто придирки, но в наши дни указатель часто будет 64-битным, а не 32-битным.
Не все классы 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. Если вы передаете указатели на объекты этого класса, не говоря уже о его подклассах, значит, вы делаете это неправильно.
Полиморфное использование не подразумевает полиморфного удаления. У класса есть множество вариантов использования виртуальных методов, но без виртуального деструктора. Рассмотрим типичное статически определенное диалоговое окно практически в любом наборе инструментов графического интерфейса. Родительское окно уничтожит дочерние объекты, и ему известен точный тип каждого из них, но все дочерние окна также будут использоваться полиморфно в любом количестве мест, таких как проверка нажатия, рисование, API специальных возможностей, которые извлекают текст для текста. голосовые движки и т. д.
Верно, но спрашивающий спрашивает, когда следует специально избегать виртуального деструктора. Для описываемого вами диалогового окна виртуальный деструктор бессмысленен, но ИМО не вреден. Я не уверен, что буду уверен, что мне никогда не придется удалять диалоговое окно с помощью указателя базового класса - например, в будущем я могу захотеть, чтобы мое родительское окно создавало свои дочерние объекты с использованием фабрик. Так что это не вопрос виртуального деструктора избегая, просто вы можете не беспокоиться о его наличии. Виртуальный деструктор в классе не подходит для вывода является вреден, хотя, потому что вводит в заблуждение.
Если у вас очень маленький класс с огромным количеством экземпляров, накладные расходы на указатель vtable могут повлиять на использование памяти вашей программой. Если в вашем классе нет других виртуальных методов, то создание невиртуального деструктора сэкономит накладные расходы.
Операция, которая будет выполняться над базовым классом и которая должна вести себя виртуально, должна быть виртуальной. Если удаление может быть выполнено полиморфно через интерфейс базового класса, тогда оно должно вести себя виртуально и быть виртуальным.
Деструктору не нужно быть виртуальным, если вы не собираетесь наследовать от класса. И даже если вы это сделаете, защищенный не виртуальный деструктор так же хорош, если удаление указателей базового класса не требуется.
Если вы абсолютно уверены, что в вашем классе нет vtable, то у вас также не должно быть виртуального деструктора.
Это редкий случай, но бывает.
Наиболее знакомым примером шаблона, который делает это, являются классы DirectX D3DVECTOR и D3DMATRIX. Это методы класса, а не функции для синтаксического сахара, но классы намеренно не имеют vtable, чтобы избежать накладных расходов на функции, потому что эти классы специально используются во внутреннем цикле многих высокопроизводительных приложений.
Хорошая причина не объявлять деструктор как виртуальный - это избавить ваш класс от добавления таблицы виртуальных функций, и вам следует избегать этого, когда это возможно.
Я знаю, что многие люди предпочитают просто объявлять деструкторы виртуальными на всякий случай. Но если в вашем классе нет других виртуальных функций, тогда действительно, действительно нет смысла иметь виртуальный деструктор. Даже если вы передадите свой класс другим людям, которые затем наследуют от него другие классы, у них не будет причин когда-либо вызывать delete для указателя, который был приведен к вашему классу - и если они это сделают, я бы посчитал это ошибкой.
Хорошо, есть одно единственное исключение, а именно, если ваш класс (неправильно) используется для выполнения полиморфного удаления производных объектов, но тогда вы - или другие ребята - надеюсь, знаете, что для этого требуется виртуальный деструктор.
Другими словами, если в вашем классе есть не виртуальный деструктор, то это очень четкое утверждение: «Не используйте меня для удаления производных объектов!»
прекрасный обзор. я отказался от своего ответа в пользу этого. +1 :)