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

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

Ответ 1

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

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

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

Ответ 2

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

С++ '98/'03

Добавление виртуального деструктора может изменить ваш класс из POD (простые старые данные) * или совокупность для не-POD. Это может остановить ваш проект от компиляции, если ваш тип класса инициализируется где-то где-то.

struct A {
  // virtual ~A ();
  int i;
  int j;
};
void foo () { 
  A a = { 0, 1 };  // Will fail if virtual dtor declared
}

В крайнем случае такое изменение может также вызывать поведение undefined, где класс используется таким образом, который требует POD, например. передавая его через параметр многоточия или используя его с memcpy.

void bar (...);
void foo (A & a) { 
  bar (a);  // Undefined behavior if virtual dtor declared
}

[* Тип POD - это тип, который имеет определенные гарантии относительно макета памяти. Стандарт действительно только говорит, что если вы должны были скопировать объект с типом POD в массив символов (или unsigned chars) и обратно, то результат будет таким же, как и исходный объект.]

Современный С++

В последних версиях С++ концепция POD была разделена между макетом класса и его конструкцией, копированием и уничтожением.

В случае с многоточием оно больше не undefined behavior теперь conditionally-supported с implementation-defined semantics (N3937 - ~ С++ '14 - 5.2.2/7):

... Передача потенциально оцененного аргумента типа класса (раздел 9), имеющего нетривиальный конструктор копирования, нетривиальный конструктор перемещения или тривиальный деструктор без соответствующего параметра, условно поддерживается с семантикой, определенной реализацией.

Объявление деструктора, отличного от =default, будет означать, что оно не является тривиальным (12.4/5)

... Деструктор тривиален, если он не предоставляется пользователю...

Другие изменения в Modern С++ уменьшают влияние проблемы инициализации агрегата при добавлении конструктора:

struct A {
  A(int i, int j);
  virtual ~A ();
  int i;

  int j;
};
void foo () { 
  A a = { 0, 1 };  // OK
}

Ответ 3

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

Ответ 4

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

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

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

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

Ответ 5

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

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

Во всех остальных случаях вы сэкономите себя на отладке, чтобы сделать dtor virtual.

Ответ 6

Не все классы С++ подходят для использования в качестве базового класса с динамическим полиморфизмом.

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

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

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

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

Ответ 7

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

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

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

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

Ответ 8

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

Ответ 9

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

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

Ответ 10

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

Это редкий случай, но это происходит.

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

Ответ 11

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

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

Ответ 12

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