Существуют ли какие-либо конкретные причины использования не виртуальных деструкторов?
Как я знаю, любой класс, который предназначен для наличия подклассов, должен быть объявлен с виртуальным деструктором, поэтому экземпляры класса могут быть уничтожены должным образом при доступе к ним через указатели.
Но почему даже можно объявить такой класс не виртуальным деструктором? Я считаю, что компилятор может решить, когда использовать виртуальные деструкторы. Итак, это надзор над дизайном С++, или я чего-то не хватает?
Ответы
Ответ 1
Существуют ли какие-либо конкретные причины использования не виртуальных деструкторов?
Да, есть.
В основном, это сводится к производительности. Виртуальная функция не может быть встроена, вместо этого вы должны сначала определить правильную функцию для вызова (которая требует информации о времени выполнения), а затем вызвать эту функцию.
В коде, чувствительном к производительности, разница между кодом и "простым" вызовом функции может иметь значение. В отличие от многих языков С++ не предполагает, что это различие тривиально.
Но почему даже можно объявить такой класс с не виртуальным деструктором?
Потому что трудно знать (для компилятора), если класс требует виртуального деструктора или нет.
Виртуальный деструктор требуется, если:
- вы вызываете
delete
по указателю
- к производному объекту через базовый класс
Когда компилятор видит определение класса:
- он не может знать, что вы намереваетесь извлечь из этого класса - вы все же можете получить из классов без виртуальных методов
- но еще более сложный: он не может знать, что вы намереваетесь вызывать
delete
в этом классе
Многие полагают, что полиморфизм требует нового экземпляра, который представляет собой просто отсутствие воображения:
class Base { public: virtual void foo() const = 0; protected: ~Base() {} };
class Derived: public Base {
public: virtual void foo() const { std::cout << "Hello, World!\n"; }
};
void print(Base const& b) { b.foo(); }
int main() {
Derived d;
print(d);
}
В этом случае нет необходимости платить за виртуальный деструктор, потому что во время уничтожения нет никакого полиморфизма.
В конце концов, это вопрос философии. Где это возможно, С++ выбирает производительность и минимальное обслуживание по умолчанию (главным исключением является RTTI).
Что касается предупреждения. Существует два предупреждения, которые могут быть использованы для выявления проблемы:
-
-Wnon-virtual-dtor
(gcc, Clang): предупреждает, когда класс с виртуальной функцией не объявляет виртуальный деструктор, если только деструктор в базовом классе не сделан protected
. Это пессимистическое предупреждение, но, по крайней мере, вы ничего не пропустите.
-
-Wdelete-non-virtual-dtor
(Clang, портирован на gcc тоже): предупреждает, когда delete
вызывается указателем на класс с виртуальными функциями, но без виртуального деструктора, если класс не помечен final
. Он имеет 0% ложноположительную ставку, но предупреждает "поздно" (и, возможно, несколько раз).
Ответ 2
Почему деструкторы не являются виртуальными по умолчанию?
http://www2.research.att.com/~bs/bs_faq2.html#virtual-dtor
Руководство № 4: деструктор базового класса должен быть открытым или виртуальным, или защищенным и не виртуальным.
http://www.gotw.ca/publications/mill18.htm
Смотрите также: http://www.erata.net/programming/virtual-destructors/
EDIT: возможно дублировать? Когда вы не должны использовать виртуальные деструкторы?
Ответ 3
Ваш вопрос в основном таков: "Почему компилятор С++ не приводит к тому, что ваш деструктор является виртуальным, если у класса есть виртуальные члены?" Логика этого вопроса заключается в том, что нужно использовать виртуальные деструкторы с классами, которые они намереваются получить.
Существует много причин, по которым компилятор С++ не пытается выдумать программиста.
-
С++ разработан по принципу получения того, за что вы платите. Если вы хотите, чтобы что-то было виртуальным, вы должны его попросить. Явное. Каждая функция класса, которая является виртуальной, должна быть явно объявлена так (если только она не переопределяет версию базового класса).
-
Если деструктор для класса с виртуальными членами был автоматически создан виртуальным, как бы вы решили сделать его не виртуальным, если это то, что вы так хотели? С++ не имеет возможности явно объявлять метод не виртуальным. Итак, как бы вы переопределили это поведение, основанное на компиляторах.
Существует ли конкретный пример использования виртуального класса с не виртуальным деструктором? Я не знаю. Может быть, там где-то вырожденный случай. Но если вам это нужно по какой-то причине, вы не сможете сказать это по вашему предложению.
Вопрос, который вы действительно должны задать себе, - это то, почему другие компиляторы не выдают предупреждений, когда класс с виртуальными членами не имеет виртуального деструктора. Что за предупреждения, в конце концов.
Ответ 4
Не виртуальный деструктор, по-видимому, имеет смысл, когда класс просто не виртуальен (примечание 1).
Однако я не вижу другого полезного использования для не виртуальных деструкторов.
И я ценю этот вопрос. Очень интересный вопрос!
EDIT:
Примечание 1:
В критичных для производительности случаях может быть полезно использовать классы без какой-либо таблицы виртуальных функций и, таким образом, без каких-либо виртуальных деструкторов вообще.
Например: подумайте о class Vector3
, который содержит только три значения с плавающей запятой. Если приложение хранит массив из них, то этот массив может храниться компактным образом.
Если нам нужна таблица виртуальных функций, и если нам даже понадобится память в куче (как в Java и co.), тогда массив будет содержать указатели на фактические элементы "SOMEWHERE" в памяти.
ИЗМЕНИТЬ 2:
Мы можем даже иметь дерево наследования классов без каких-либо виртуальных методов вообще.
Почему?
Потому что, даже если с "виртуальными" методами может показаться общим и предпочтительным случаем, это НЕ единственный случай, который мы, человечество, можем себе представить.
Как и во многих деталях этого языка, С++ предлагает вам выбор. Вы можете выбрать один из предоставленных вариантов, обычно вы выбираете тот, который выбирает любой другой. Но иногда вы не хотите этого варианта!
В нашем примере класс Vector3 может наследовать от класса Vector2 и не будет иметь накладных расходов на вызовы виртуальных функций. Думал, что этот пример не очень хорош;)
Ответ 5
Еще одна причина, о которой я не упоминал здесь, - это границы DLL: вы хотите использовать один и тот же распределитель для освобождения объекта, который вы использовали для его выделения.
Если методы живут в DLL, но клиентский код создает объект с помощью прямого new
, тогда клиентский распределитель используется для получения памяти для объекта, но объект заполняется с помощью vtable из DLL, которая указывает на деструктор, который использует распределитель, с которым связана DLL, чтобы освободить объект.
При подклассификации классов из DLL в клиенте проблема исчезает, поскольку виртуальный деструктор из DLL не используется.