Коли не слід використовувати віртуальні деструктори?


Відповіді:


72

Немає необхідності використовувати віртуальний деструктор, коли будь-що з наведеного нижче відповідає дійсності:

  • Немає наміру виводити з цього заняття
  • Немає екземпляра в купі
  • Немає наміру зберігати в покажчику суперкласу

Немає конкретних причин, щоб уникнути цього, якщо ви справді так не потребуєте пам’яті.


25
Це невдала відповідь. "Немає потреби" відрізняється від "не повинен", а "немає наміру" відрізняється від "зробив неможливим".
Програміст для Windows

5
Також додайте: немає наміру видаляти екземпляр за допомогою покажчика базового класу.
Адам Розенфілд,

9
Це насправді не відповідає на питання. Де ваша вагома причина не використовувати віртуальний dtor?
mxcl

9
Я думаю, що коли немає потреби щось робити, це вагома причина не робити цього. Він дотримується принципу простого дизайну XP.
вересень

12
Кажучи, що у вас немає наміру, ви робите величезне припущення про те, як ваш клас звикне. Мені здається, що найпростішим рішенням у більшості випадків (а це має бути за замовчуванням) має бути наявність віртуальних деструкторів, і уникати їх слід лише в тому випадку, якщо у вас є конкретна причина цього не робити. Тому мені все ще цікаво, що було б вагомою причиною.
ckarras

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)

... Деструктор є тривіальним, якщо він не надається користувачем ...

Інші зміни сучасного C ++ зменшують вплив сукупної проблеми ініціалізації, оскільки конструктор може бути доданий:

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

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

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

шановний Річард, чи не можеш ти прокоментувати ще трохи того, що ти написав. Я не розумію вашої точки зору, але, здається, єдиний цінний момент, який я знайшов, погугливши) Або ви можете дати посилання на більш детальне пояснення?
Джон Сміт

1
@JohnSmith Я оновив відповідь. Сподіваємось, це допомагає.
Річард Корден,

28

Я оголошую віртуальний деструктор тоді і тільки тоді, коли у мене є віртуальні методи. Після того, як у мене є віртуальні методи, я не довіряю собі уникати створення його в купі або збереження вказівника на базовий клас. Обидві ці операції є надзвичайно поширеними і часто безшумно виточують ресурси, якщо деструктор не оголошено віртуальним.


3
І насправді, на gcc є опція попередження, яка попереджає саме про цей випадок (віртуальні методи, але відсутність віртуального dtor).
CesarB

6
Тоді ви не ризикуєте втратити пам'ять, якщо ви походите з класу, незалежно від того, чи є у вас інші віртуальні функції?
Mag Roader

1
Погоджуюсь з маг. Це використання віртуального деструктора та / або віртуального методу є окремими вимогами. Віртуальний деструктор забезпечує можливість класу виконувати очищення (наприклад, видаляти пам'ять, закривати файли тощо ...) І також забезпечує виклик конструкторів усіх його членів.
користувач48956

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 біти замість 32.
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
Правда, але запитувальник запитує, коли слід спеціально уникати віртуального деструктора. Для діалогового вікна, яке ви описуєте, віртуальний деструктор безглуздий, але IMO не шкідливий. Я не впевнений, що буду впевнений, що мені ніколи не доведеться видаляти діалогове вікно за допомогою вказівника базового класу - наприклад, я можу в майбутньому захотіти, щоб моє батьківське вікно створювало свої дочірні об'єкти за допомогою фабрик. Тож мова не йде про те, щоб уникнути віртуального деструктора, а лише про те, що ви, можливо, не потурбуєтесь його мати. Віртуальний деструктор класу, непридатного для виведення , шкідливий, однак, оскільки він вводить в оману.
Steve Jessop

4

Важливою причиною не оголошувати деструктор віртуальним - це коли це рятує ваш клас від додавання віртуальної таблиці функцій, і вам слід уникати цього, коли це можливо.

Я знаю, що багато людей воліють просто завжди оголошувати деструктори віртуальними, щоб бути в безпеці. Але якщо ваш клас не має жодних інших віртуальних функцій, тоді насправді немає сенсу мати віртуальний деструктор. Навіть якщо ви віддасте свій клас іншим людям, які потім виводять з нього інші класи, у них не буде жодної причини коли-небудь викликати delete на покажчику, який був оновлений для вашого класу - і якщо вони це роблять, я вважав би це помилкою.

Гаразд, є один єдиний виняток, а саме, якщо ваш клас (неправильно) використовується для виконання поліморфного видалення похідних об’єктів, але тоді ви - або інші хлопці - сподіваємось, знаєте, що для цього потрібен віртуальний деструктор.

Іншими словами, якщо у вашому класі є невіртуальний деструктор, це дуже чітке твердження: "Не використовуй мене для видалення похідних об'єктів!"


3

Якщо у вас дуже маленький клас із величезною кількістю екземплярів, накладні витрати на покажчик vtable можуть змінити використання пам'яті вашої програми. Поки ваш клас не має жодних інших віртуальних методів, перетворення деструктора в невіртуальний спосіб заощадить це.


1

Я зазвичай оголошую деструктор віртуальним, але якщо у вас є критичний для продуктивності код, який використовується у внутрішньому циклі, ви можете уникнути пошуку віртуальної таблиці. Це може бути важливим у деяких випадках, наприклад, перевірка зіткнень. Але будьте обережні щодо того, як ви знищуєте ці об’єкти, якщо використовуєте спадщину, або ви знищите лише половину об’єкта.

Зверніть увагу, що пошук віртуальної таблиці відбувається для об’єкта, якщо будь-який метод на цьому об’єкті є віртуальним. Тож немає сенсу видаляти віртуальну специфікацію з деструктора, якщо у вас є інші віртуальні методи в класі.


1

Якщо ви абсолютно позитивно повинні переконатись, що у вашому класі немає vtable, тоді ви також не повинні мати віртуального деструктора.

Це рідкісний випадок, але він трапляється.

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


0

Для операції, яка буде виконуватися на базовому класі, і яка повинна вести себе віртуально, повинна бути віртуальна. Якщо видалення можна виконати поліморфно через інтерфейс базового класу, то воно повинно вести себе віртуально і бути віртуальним.

Деструктору не потрібно бути віртуальним, якщо ви не маєте наміру походити з класу. І навіть якщо ви це зробите, захищений невіртуальний деструктор так само добре, якщо видалення покажчиків базового класу не потрібно .


-7

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


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

1
У C ++ ви не несете відповідальності за те, щоб я не успадковував ваші класи, які ви задокументували як непридатні для використання в якості базових класів. Я несу відповідальність використовувати спадщину з обережністю. Звичайно, якщо керівництво з домашнього стилю не скаже інакше.
Стів Джессоп,

1
... просто створення деструктора віртуальним не означає, що клас обов'язково працюватиме як базовий клас. Отже, позначення його віртуальним "просто тому, що" замість того, щоб робити таку оцінку, пише чек, який мій код не може отримати.
Steve Jessop
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.