Почему я должен объявлять виртуальный деструктор для абстрактного класса в C ++?

165

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

Kevin
источник

Ответы:

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; 
}
Airsource Ltd
источник
4
delete pвызывает неопределенное поведение. Звонить не гарантируется Interface::~Interface.
Mankarse
@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

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

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

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

[См. Пункт 4 в этой статье: http://www.gotw.ca/publications/mill18.htm ]

Йоханнес Шауб - Литб
источник
Ключом к тому, чтобы заставить ваш ответ работать, является «на котором удаление не вызывается». Обычно, если у вас есть абстрактный базовый класс, предназначенный для интерфейса, в интерфейсе класса вызывается delete.
Джон Диблинг
Как сказал выше Джон, то, что вы предлагаете, довольно опасно. Вы полагаетесь на предположение, что клиенты вашего интерфейса никогда не уничтожат объект, зная только базовый тип. Единственный способ гарантировать это, если он не виртуальный, - защитить dtor абстрактного класса.
Мишель
Мишель, я так и сказал :) «Если вы сделаете это, вы сделаете свой деструктор защищенным. Если вы это сделаете, клиенты не смогут удалить, используя указатель на этот интерфейс». и действительно, он не полагается на клиентов, но должен заставить его говорить клиентам: «Вы не можете делать ...». Я не вижу никакой опасности
Йоханнес Шауб - Lit
я исправил неправильную формулировку своего ответа сейчас. теперь он прямо заявляет, что не полагается на клиентов. на самом деле я думал, что очевидно, что полагаться на то, что клиенты что-то делают, так или иначе. спасибо :)
Йоханнес Шауб -
2
+1 за упоминание защищенных деструкторов, которые являются другим «выходом» из проблемы случайного вызова неправильного деструктора при удалении указателя на базовый класс.
j_random_hacker
23

Я решил провести небольшое исследование и попытаться обобщить ваши ответы. Следующие вопросы помогут вам решить, какой деструктор вам нужен:

  1. Ваш класс предназначен для использования в качестве базового класса?
    • No: Объявите открытый не виртуальный деструктор, чтобы избежать v-указателя на каждый объект класса * .
    • Да: прочитайте следующий вопрос.
  2. Ваш базовый класс абстрактный? (т.е. какие-либо виртуальные чистые методы?)
    • Нет: попробуйте сделать базовый класс абстрактным, изменив иерархию классов.
    • Да: прочитайте следующий вопрос.
  3. Вы хотите разрешить полиморфное удаление через базовый указатель?
    • Нет: объявите защищенный виртуальный деструктор, чтобы предотвратить нежелательное использование.
    • Да: объявить общедоступный виртуальный деструктор (в этом случае нет накладных расходов).

Надеюсь, это поможет.

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

Ссылки:

davidag
источник
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
OJ.
источник
Правда, на самом деле в этом примере это может быть не просто утечка памяти, но, возможно, сбой: - /
Эван Теран
7

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

Так, например:

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

плохо сформирован, если Baseне имеет виртуального деструктора, потому что он попытается удалить объект, как если бы он был Base *.

Эван Теран
источник
не хотите ли вы исправить boost :: shared_pointer p (new Derived), чтобы оно выглядело как boost :: shared_pointer <Base> p (new Derived); ? может быть, тогда госзакупки поймут ваш ответ и проголосуют
Йоханнес Шауб -
РЕДАКТИРОВАТЬ: «Codified» пару частей, чтобы сделать угловые скобки видимыми, как предложено litb.
j_random_hacker
@EvanTeran: я не уверен, изменилось ли это с тех пор, как ответ был первоначально опубликован (документация Boost по адресу boost.org/doc/libs/1_52_0/libs/smart_ptr/shared_ptr.htm предполагает, что это возможно), но это не так в эти дни shared_ptrон пытается удалить объект, как если бы он был Base *- он запоминает тип того, с чем вы его создали. См. Ссылочную ссылку, в частности, бит, который говорит: «Деструктор будет вызывать delete с тем же указателем, в комплекте с его исходным типом, даже если у T нет виртуального деструктора или он пуст».
Стюарт Голодец
@StuartGolodetz: Хм, вы можете быть правы, но я, честно говоря, не уверен. В этом контексте он все еще может быть плохо сформирован из-за отсутствия виртуального деструктора. Это стоит посмотреть.
Эван Теран
@EvanTeran: В случае, если это полезно - stackoverflow.com/questions/3899790/shared-ptr-magic .
Стюарт Голодец
5

Это не только хорошая практика. Это правило № 1 для любой иерархии классов.

  1. Самый базовый класс иерархии в C ++ должен иметь виртуальный деструктор

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

Animal* pAnimal = GetAnimal();
delete pAnimal;

Предположим, что Animal является абстрактным классом. Единственный способ, которым C ++ знает правильный деструктор для вызова, - это диспетчеризация виртуального метода. Если деструктор не является виртуальным, он просто вызывает деструктор Animal и не уничтожает объекты в производных классах.

Причина создания виртуального деструктора в базовом классе заключается в том, что он просто удаляет выбор из производных классов. Их деструктор становится виртуальным по умолчанию.

JaredPar
источник
2
Я в основном согласен с вами, потому что обычно при определении иерархии вы хотите иметь возможность ссылаться на производный объект, используя указатель / ссылку базового класса. Но это не всегда так, и в этих других случаях может быть достаточно сделать вместо этого защищенный базовый класс dtor.
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%, особенно в отношении проверки контрактов в розничном коде. Я имею в виду, что бывают случаи, когда вы знаете, что вам не нужен виртуальный дтор. Пример: теги классов для отправки шаблона. Они имеют размер 0, вы используете наследование только для указания специализации.
j_random_hacker
3

Ответ прост, вам нужно, чтобы он был виртуальным, иначе базовый класс не был бы полным полиморфным классом.

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

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

fatma.ekici
источник