Чи є коли-небудь вагома причина не оголошувати віртуальний деструктор для класу? Коли слід спеціально уникати написання?
Чи є коли-небудь вагома причина не оголошувати віртуальний деструктор для класу? Коли слід спеціально уникати написання?
Відповіді:
Немає необхідності використовувати віртуальний деструктор, коли будь-що з наведеного нижче відповідає дійсності:
Немає конкретних причин, щоб уникнути цього, якщо ви справді так не потребуєте пам’яті.
Щоб відповісти на цей питання в явному вигляді, тобто , коли ви повинні НЕ оголосити віртуальний деструктор.
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)
... Деструктор є тривіальним, якщо він не надається користувачем ...
Інші зміни сучасного 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. Якщо ви передаєте вказівники на об’єкти цього класу, не кажучи вже про його підкласи, тоді ви робите це неправильно.
Важливою причиною не оголошувати деструктор віртуальним - це коли це рятує ваш клас від додавання віртуальної таблиці функцій, і вам слід уникати цього, коли це можливо.
Я знаю, що багато людей воліють просто завжди оголошувати деструктори віртуальними, щоб бути в безпеці. Але якщо ваш клас не має жодних інших віртуальних функцій, тоді насправді немає сенсу мати віртуальний деструктор. Навіть якщо ви віддасте свій клас іншим людям, які потім виводять з нього інші класи, у них не буде жодної причини коли-небудь викликати delete на покажчику, який був оновлений для вашого класу - і якщо вони це роблять, я вважав би це помилкою.
Гаразд, є один єдиний виняток, а саме, якщо ваш клас (неправильно) використовується для виконання поліморфного видалення похідних об’єктів, але тоді ви - або інші хлопці - сподіваємось, знаєте, що для цього потрібен віртуальний деструктор.
Іншими словами, якщо у вашому класі є невіртуальний деструктор, це дуже чітке твердження: "Не використовуй мене для видалення похідних об'єктів!"
Якщо у вас дуже маленький клас із величезною кількістю екземплярів, накладні витрати на покажчик vtable можуть змінити використання пам'яті вашої програми. Поки ваш клас не має жодних інших віртуальних методів, перетворення деструктора в невіртуальний спосіб заощадить це.
Я зазвичай оголошую деструктор віртуальним, але якщо у вас є критичний для продуктивності код, який використовується у внутрішньому циклі, ви можете уникнути пошуку віртуальної таблиці. Це може бути важливим у деяких випадках, наприклад, перевірка зіткнень. Але будьте обережні щодо того, як ви знищуєте ці об’єкти, якщо використовуєте спадщину, або ви знищите лише половину об’єкта.
Зверніть увагу, що пошук віртуальної таблиці відбувається для об’єкта, якщо будь-який метод на цьому об’єкті є віртуальним. Тож немає сенсу видаляти віртуальну специфікацію з деструктора, якщо у вас є інші віртуальні методи в класі.
Якщо ви абсолютно позитивно повинні переконатись, що у вашому класі немає vtable, тоді ви також не повинні мати віртуального деструктора.
Це рідкісний випадок, але він трапляється.
Найбільш відомим прикладом шаблону, який робить це, є класи DirectX D3DVECTOR та D3DMATRIX. Це методи класів замість функцій для синтаксичного цукру, але класи навмисно не мають vtable, щоб уникнути накладних витрат на функцію, оскільки ці класи спеціально використовуються у внутрішньому циклі багатьох високопродуктивних програм.
Для операції, яка буде виконуватися на базовому класі, і яка повинна вести себе віртуально, повинна бути віртуальна. Якщо видалення можна виконати поліморфно через інтерфейс базового класу, то воно повинно вести себе віртуально і бути віртуальним.
Деструктору не потрібно бути віртуальним, якщо ви не маєте наміру походити з класу. І навіть якщо ви це зробите, захищений невіртуальний деструктор так само добре, якщо видалення покажчиків базового класу не потрібно .
Відповідь результативності єдина, яку я знаю, імовірно, є правдивою. Якщо ви виміряли і виявили, що де-віртуалізація ваших деструкторів дійсно пришвидшує процес, то, ймовірно, у вас є інші речі цього класу, які теж потребують прискорення, але на даний момент є ще важливіші міркування. Одного разу хтось збирається виявити, що ваш код створить для них приємний базовий клас і заощадить тиждень роботи. Краще переконайтеся, що вони виконують роботу цього тижня, копіюючи та вставляючи ваш код, замість того, щоб використовувати ваш код як основу. Краще переконайтеся, що ви зробите деякі важливі методи приватними, щоб ніхто ніколи не міг успадкувати від вас.