Когда не следует использовать виртуальные деструкторы?


Ответы:


72

Нет необходимости использовать виртуальный деструктор, если верно любое из следующих утверждений:

  • Нет намерения выводить из него классы
  • Нет экземпляра в куче
  • Нет намерения хранить указатель суперкласса

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


25
Это плохой ответ. «Нет необходимости» отличается от «не следует», а «без намерения» отличается от «сделать невозможным».
Программист Windows,

5
Также добавьте: нет намерения удалять экземпляр с помощью указателя базового класса.
Адам Розенфилд

9
Это не совсем ответ на вопрос. Где у вас есть веская причина не использовать виртуальный дтор?
mxcl

9
Я думаю, что когда нет необходимости что-то делать, это хороший повод не делать этого. Это следование принципу простого дизайна XP.
Сентябрь

12
Говоря, что у вас «нет намерения», вы делаете огромное предположение о том, как ваш класс будет использоваться. Мне кажется, что самое простое решение в большинстве случаев (которое, следовательно, должно быть по умолчанию) должно заключаться в том, чтобы иметь виртуальные деструкторы и избегать их только в том случае, если у вас есть конкретная причина не делать этого. Так что мне все еще любопытно, что было бы хорошей причиной.
ckarras 02

68

Чтобы ответить на этот вопрос в явном виде, то есть , когда вы должны не объявить виртуальный деструктор.

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
}

1
Вы правы, и я ошибался, производительность - не единственная причина. Но это показывает, что в остальном я был прав: программисту класса лучше было бы включить код, чтобы предотвратить наследование класса кем-либо еще.
Программист Windows,

дорогой Ричард, не могли бы вы прокомментировать еще немного то, что вы написали. Я не понимаю вашу точку зрения, но это, кажется, единственный ценный момент, который я нашел путем поиска в Google) А может быть вы можете дать ссылку на более подробное объяснение?
John Smith

1
@JohnSmith Я обновил ответ. Надеюсь, это поможет.
Ричард Корден

28

Я объявляю виртуальный деструктор тогда и только тогда, когда у меня есть виртуальные методы. Когда у меня есть виртуальные методы, я не верю себе, чтобы избежать создания их экземпляров в куче или сохранения указателя на базовый класс. Обе эти операции являются чрезвычайно распространенными и часто вызывают утечку ресурсов незаметно, если деструктор не объявлен виртуальным.


3
И, действительно, в gcc есть опция предупреждения, которая предупреждает именно об этом случае (виртуальные методы, но не виртуальный dtor).
CesarB

6
Разве тогда вы не рискуете утечки памяти, если вы производите от класса, независимо от того, есть ли у вас другие виртуальные функции?
Mag Roader

1
Я согласен с mag. Это использование виртуального деструктора и / или виртуального метода - это отдельные требования. Виртуальный деструктор предоставляет классу возможность выполнять очистку (например, удалять память, закрывать файлы и т. Д.) И также обеспечивает вызов конструкторов всех его членов.
user48956

7

Виртуальный деструктор необходим всякий раз, когда есть шанс deleteвызвать указатель на объект подкласса с типом вашего класса. Это гарантирует, что правильный деструктор вызывается во время выполнения, и компилятору не нужно знать класс объекта в куче во время компиляции. Например, предположим, что Bэто подкласс A:

A *x = new B;
delete x;     // ~B() called, even though x has type A*

Если ваш код не критичен к производительности, было бы разумно добавить виртуальный деструктор к каждому базовому классу, который вы пишете, просто для безопасности.

Однако, если вы обнаружили, что deleteв замкнутом цикле много объектов, могут быть заметны издержки производительности при вызове виртуальной функции (даже пустой). Компилятор обычно не может встроить эти вызовы, и процессору может быть сложно предугадать, куда идти. Вряд ли это сильно повлияет на производительность, но об этом стоит упомянуть.


«Если ваш код не критичен к производительности, было бы разумно добавить виртуальный деструктор к каждому базовому классу, который вы пишете, просто для безопасности». следует уделять больше внимания каждому ответу, который я вижу
csguy

5

Виртуальные функции означают, что каждый выделенный объект увеличивает стоимость памяти на указатель таблицы виртуальных функций.

Поэтому, если ваша программа включает выделение очень большого количества некоторого объекта, было бы целесообразно избегать всех виртуальных функций, чтобы сэкономить дополнительные 32 бита на каждый объект.

Во всех остальных случаях вы избавите себя от лишних хлопот, сделав dtor виртуальным.


1
Просто придирки, но в наши дни указатель часто будет 64-
Head Geek

5

Не все классы 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. Если вы передаете указатели на объекты этого класса, не говоря уже о его подклассах, значит, вы делаете это неправильно.


2
Полиморфное использование не подразумевает полиморфного удаления. У класса есть множество вариантов использования виртуальных методов, но без виртуального деструктора. Рассмотрим типичное статически определенное диалоговое окно практически в любом наборе инструментов графического интерфейса. Родительское окно уничтожит дочерние объекты, и ему известен точный тип каждого из них, но все дочерние окна также будут использоваться полиморфно в любом количестве мест, таких как проверка нажатия, рисование, API специальных возможностей, которые извлекают текст для текста. к речевым движкам и т. д.
Бен Фойгт,

4
Верно, но спрашивающий спрашивает, когда следует специально избегать виртуального деструктора. Для описываемого вами диалогового окна виртуальный деструктор бессмыслен, но ИМО не вреден. Я не уверен, что буду уверен, что мне никогда не придется удалять диалоговое окно с помощью указателя базового класса - например, в будущем я могу захотеть, чтобы мое родительское окно создавало свои дочерние объекты с использованием фабрик. Так что вопрос не в том, чтобы избежать виртуального деструктора, просто вы можете не беспокоиться о его наличии. Виртуальный деструктор класса, не подходящего для наследования , вреден, потому что вводит в заблуждение.
Стив Джессоп,

4

Хорошая причина не объявлять деструктор как виртуальный - это избавить ваш класс от добавления таблицы виртуальных функций, и вам следует избегать этого, когда это возможно.

Я знаю, что многие люди предпочитают просто объявлять деструкторы виртуальными на всякий случай. Но если в вашем классе нет других виртуальных функций, тогда действительно нет смысла иметь виртуальный деструктор. Даже если вы передадите свой класс другим людям, которые затем наследуют от него другие классы, у них не будет причин когда-либо вызывать удаление указателя, который был приведен к вашему классу - и если они это сделают, я буду считать это ошибкой.

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

Другими словами, если в вашем классе есть не виртуальный деструктор, то это очень четкое утверждение: «Не используйте меня для удаления производных объектов!»


3

Если у вас очень маленький класс с огромным количеством экземпляров, накладные расходы на указатель vtable могут повлиять на использование памяти вашей программой. Если в вашем классе нет других виртуальных методов, то создание невиртуального деструктора сэкономит накладные расходы.


1

Я обычно объявляю деструктор виртуальным, но если у вас есть критический для производительности код, который используется во внутреннем цикле, вы можете избежать поиска в виртуальной таблице. В некоторых случаях это может быть важно, например, при проверке столкновений. Но будьте осторожны с тем, как вы уничтожаете эти объекты, если вы используете наследование, иначе вы уничтожите только половину объекта.

Обратите внимание, что поиск в виртуальной таблице выполняется для объекта, если какой-либо метод этого объекта является виртуальным. Таким образом, нет смысла удалять виртуальную спецификацию в деструкторе, если у вас есть другие виртуальные методы в классе.


1

Если вы абсолютно уверены, что ваш класс не имеет vtable, тогда у вас также не должно быть виртуального деструктора.

Это редкий случай, но бывает.

Наиболее знакомым примером шаблона, который делает это, являются классы DirectX D3DVECTOR и D3DMATRIX. Это методы класса, а не функции для синтаксического сахара, но классы намеренно не имеют vtable, чтобы избежать накладных расходов на функции, поскольку эти классы специально используются во внутреннем цикле многих высокопроизводительных приложений.


0

Операция, которая будет выполняться над базовым классом и которая должна вести себя виртуально, должна быть виртуальной. Если удаление может быть выполнено полиморфно через интерфейс базового класса, оно должно вести себя виртуально и быть виртуальным.

Деструктору не обязательно быть виртуальным, если вы не собираетесь наследовать от класса. И даже если вы это сделаете, защищенный не виртуальный деструктор будет столь же хорош, если удаление указателей базового класса не требуется .


-7

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


Полиморфизм, безусловно, замедлит работу. Сравните это с ситуацией, когда нам нужен полиморфизм и мы решили не делать этого, он будет еще медленнее. Пример: мы реализуем всю логику в деструкторе базового класса, используя RTTI и оператор switch для очистки ресурсов.
сентябрь

1
В C ++ вы не обязаны мешать мне наследовать ваши классы, которые, как вы задокументировали, не подходят для использования в качестве базовых классов. Я обязан осторожно использовать наследование. Если, конечно, руководство по домашнему стилю не говорит иначе.
Стив Джессоп,

1
... просто создание виртуального деструктора не означает, что класс обязательно будет правильно работать как базовый класс. Так что пометить его виртуально «просто потому, что» вместо того, чтобы делать эту оценку, означает выписать чек, который мой код не может обналичить.
Стив Джессоп,
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.