Чому я повинен оголошувати віртуальний деструктор для абстрактного класу в C ++?


165

Я знаю, що декларувати віртуальні деструктори для базових класів у C ++ є хорошою практикою, але чи завжди важливо оголошувати virtualдеструктори навіть для абстрактних класів, які функціонують як інтерфейси? Наведіть, будь ласка, кілька причин та прикладів.

Відповіді:


196

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

Наприклад

class Interface
{
   virtual void doSomething() = 0;
};

class Derived : public Interface
{
   Derived();
   ~Derived() 
   {
      // Do some important cleanup...
   }
};

void myFunc(void)
{
   Interface* p = new Derived();
   // The behaviour of the next line is undefined. It probably 
   // calls Interface::~Interface, not Derived::~Derived
   delete p; 
}

4
delete pпосилається на невизначену поведінку. Телефонувати не гарантовано Interface::~Interface.
Манкарсе

@Mankarse: чи можете ви пояснити, що обумовлює його невизначеність? Якби Derived не реалізував власний деструктор, чи все-таки це буде невизначена поведінка?
Ponkadoodle

14
@Wallacoloo: Це не визначено з - за [expr.delete]/: ... if the static type of the object to be deleted is different from its dynamic type, ... the static type shall have a virtual destructor or the behavior is undefined. .... Це все ще буде невизначено, якби Derived використовував неявно створений деструктор.
Манкарсе

37

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

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

Зрештою, у вас не буде таблиці віртуальних методів, і в кінцевому підсумку ви будете сигналізувати своїм клієнтам про намір зробити його незнімним через вказівник на нього, тому ви справді маєте підстави не оголошувати його віртуальним у цих випадках.

[Дивіться пункт 4 у цій статті: http://www.gotw.ca/publications/mill18.htm ]


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

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

Мішель, я сказав так :) "Якщо ви це зробите, ви робите свій деструктор захищеним. Якщо ви це зробите, клієнти не зможуть видалити, використовуючи вказівник на цей інтерфейс". Дійсно, він не покладається на клієнтів, але він повинен примусити його говорити клієнтам "ти не можеш робити ...". Я не бачу жодної небезпеки
Йоханнес Шауб - запалений

Я виправив погану формулювання своєї відповіді зараз. він прямо заявляє, що тепер не покладається на клієнтів. насправді я думав, що очевидно, що покладатися на клієнтів, що роблять щось, все одно не виходить. дякую :)
Йоханнес Шауб - ліб

2
+1 для згадування захищених деструкторів, які є іншим "виходом" із проблеми випадкового виклику неправильного деструктора при видаленні вказівника до базового класу.
j_random_hacker

23

Я вирішив зробити кілька досліджень і спробувати узагальнити ваші відповіді. Наступні питання допоможуть вам вирішити, який тип деструктора вам потрібен:

  1. Чи призначений ваш клас використовуватись як базовий клас?
    • Ні: оголосити публічний невіртуальний деструктор, щоб уникнути v-покажчика на кожен об'єкт класу * .
    • Так: Прочитайте наступне запитання.
  2. Ваш базовий клас абстрактний? (тобто будь-які віртуальні чисті методи?)
    • Ні: Спробуйте зробити базовий клас абстрактним шляхом перестановки ієрархії класів
    • Так: Прочитайте наступне запитання.
  3. Ви хочете дозволити поліморфне видалення через базовий покажчик?
    • Ні: оголосити захищений віртуальний деструктор, щоб запобігти небажаному використанню.
    • Так: оголосити загальнодоступний віртуальний деструктор (в цьому випадку немає накладних витрат).

Я сподіваюся, що це допомагає.

* Важливо зауважити, що в C ++ немає можливості позначати клас як остаточний (тобто не підкласифікований), тому у випадку, якщо ви вирішили оголосити свого деструктора невіртуальним та загальнодоступним, не забудьте прямо застерегти своїх колег-програмістів від походить від вашого класу.

Список літератури:


11
Ця відповідь частково застаріла, тепер у C ++ є остаточне ключове слово.
Етьєн

10

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

Отже, ви відкриваєте потенціал для витоку пам'яті

class IFoo
{
  public:
    virtual void DoFoo() = 0;
};

class Bar : public IFoo
{
  char* dooby = NULL;
  public:
    virtual void DoFoo() { dooby = new char[10]; }
    void ~Bar() { delete [] dooby; }
};

IFoo* baz = new Bar();
baz->DoFoo();
delete baz; // memory leak - dooby isn't deleted

Правда, насправді в цьому прикладі це може не просто витік пам’яті, а можливо і аварія: - /
Еван Теран

7

Це не завжди потрібно, але я вважаю це гарною практикою. Що це робить, чи дозволяє похідний об'єкт безпечно видалити через покажчик базового типу.

Так, наприклад:

Base *p = new Derived;
// use p as you see fit
delete p;

неправильно сформований, якщо у Baseнього немає віртуального деструктора, тому що він буде намагатися видалити об'єкт так, ніби він був Base *.


ви не хочете виправити boost :: shared_pointer p (новий Отриманий), щоб виглядати boost :: shared_pointer <Base> p (новий Отриманий); ? можливо, ppl зрозуміє вашу відповідь тоді і проголосує
Йоханнес Шауб - ліб

EDIT: "Кодифіковано" пару частин, щоб зробити кутові дужки видимими, як запропоновано лампа.
j_random_hacker

@EvanTeran: Я не впевнений, чи змінилося це з моменту сповіщення відповіді (документація Boost на boost.org/doc/libs/1_52_0/libs/smart_ptr/shared_ptr.htm пропонує це), але це неправда ці дні shared_ptrнамагаються видалити об'єкт так, ніби він був Base *- він пам’ятає тип речі, з якої ви створили його. Дивіться посилане посилання, зокрема біт, на якому написано "Деструктор викличе видалення з тим самим покажчиком у комплекті з його початковим типом, навіть коли T не має віртуального деструктора або недійсний".
Стюарт Голодець

@StuartGolodetz: Хм, ви можете мати рацію, але я чесно не впевнений. У цьому контексті він все ще може бути погано сформований через відсутність віртуального деструктора. Варто розібратися.
Еван Теран


5

Це не лише добра практика. Це правило №1 для будь-якої ієрархії класів.

  1. Основний клас ієрархії в C ++ повинен мати віртуальний деструктор

Тепер для Чому. Візьміть типову ієрархію тварин. Віртуальні деструктори проходять віртуальну розсилку так само, як і будь-який інший метод виклику. Візьмемо наступний приклад.

Animal* pAnimal = GetAnimal();
delete pAnimal;

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

Причиною зробити деструктор віртуальним у базовому класі є те, що він просто знімає вибір із похідних класів. Їх деструктор за замовчуванням стає віртуальним.


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

@j_random_hacker, що робить його захищеним, не захистить вас від неправильних внутрішніх
видалень

1
@JaredPar: Це правильно, але принаймні ви можете нести відповідальність за власний код - найважче - переконатися, що клієнтський код не може призвести до вибуху вашого коду. (Аналогічно, приватний член даних не заважає внутрішньому коду робити щось дурне з цим членом.)
j_random_hacker

@j_random_hacker, вибачте за відповідь за допомогою повідомлення в блозі, але це дійсно відповідає цьому сценарію. blogs.msdn.com/jaredpar/archive/2008/03/24/…
JaredPar

@JaredPar: Відмінна посада, я погоджуюся з вами на 100%, особливо щодо перевірки контрактів у роздрібному коді. Я просто маю на увазі, що є випадки, коли ви знаєте, що вам не потрібен віртуальний dtor. Приклад: класи тегів для відправки шаблонів. Вони мають 0 розмір, ви використовуєте спадщину лише для позначення спеціалізацій.
j_random_hacker

3

Відповідь проста: вам потрібно, щоб вона була віртуальною, інакше базовий клас не був би повним поліморфним класом.

    Base *ptr = new Derived();
    delete ptr; // Here the call order of destructors: first Derived then Base.

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

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