Por que devo declarar um destruidor virtual para uma classe abstrata em C ++?

165

Sei que é uma boa prática declarar destruidores virtuais para classes base em C ++, mas é sempre importante declarar virtualdestruidores mesmo para classes abstratas que funcionam como interfaces? Forneça alguns motivos e exemplos do motivo.

Kevin
fonte

Respostas:

196

É ainda mais importante para uma interface. Qualquer usuário da sua classe provavelmente conterá um ponteiro para a interface, não um ponteiro para a implementação concreta. Quando eles vierem para excluí-lo, se o destruidor não for virtual, eles chamarão o destruidor da interface (ou o padrão fornecido pelo compilador, se você não especificou um), não o destruidor da classe derivada. Vazamento de memória instantânea.

Por exemplo

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
fonte
4
delete pinvoca comportamento indefinido. Não é garantido ligar Interface::~Interface.
Mankarse 23/01
@ Makarse: você pode explicar o que faz com que seja indefinido? Se o Derived não implementasse seu próprio destruidor, ainda seria um comportamento indefinido?
Ponkadoodle
14
@Wallacoloo: ela é indefinida por causa de [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. .... Ainda seria indefinido se Derived usasse um destruidor gerado implicitamente.
Mankarse
37

A resposta para sua pergunta é frequentemente, mas nem sempre. Se a sua classe abstrata proíbe que os clientes chamem delete em um ponteiro para ele (ou, se assim o desejar na documentação), você é livre para não declarar um destruidor virtual.

Você pode proibir que os clientes chamem delete em um ponteiro para ele, protegendo seu destruidor. Trabalhando assim, é perfeitamente seguro e razoável omitir um destruidor virtual.

Você acabará por não ter uma tabela de método virtual e sinalizará a seus clientes sua intenção de torná-la não excluída por meio de um ponteiro para ela, portanto, você tem motivos para não declara-la virtual nesses casos.

[Veja o item 4 deste artigo: http://www.gotw.ca/publications/mill18.htm ]

Johannes Schaub - litb
fonte
A chave para fazer sua resposta funcionar é "na qual a exclusão não é chamada". Normalmente, se você tiver uma classe base abstrata projetada para ser uma interface, a exclusão será chamada na classe de interface.
John Dibling
Como John acima apontou, o que você está sugerindo é bastante perigoso. Você está confiando na suposição de que os clientes da sua interface nunca destruirão um objeto que conhece apenas o tipo base. A única maneira de garantir que, se não for virtual, é proteger o dtor da classe abstrata.
Michel
Michel, eu disse isso :) "Se você fizer isso, você protegerá seu destruidor. Se você fizer isso, os clientes não poderão excluir usando um ponteiro para essa interface." e, de fato, não está confiando nos clientes, mas deve impor isso dizendo aos clientes "você não pode fazer ...". Não vejo perigo
Johannes Schaub - litb 28/11
Eu consertei as palavras ruins da minha resposta agora. afirma explicitamente agora que não depende dos clientes. na verdade, eu pensei que era óbvio que depender de clientes fazendo algo está fora do caminho de qualquer maneira. obrigado :)
Johannes Schaub - litb
2
+1 por mencionar destruidores protegidos, que são a outra "saída" do problema de chamar acidentalmente o destruidor errado ao excluir um ponteiro para uma classe base.
j_random_hacker
23

Decidi fazer uma pesquisa e tentar resumir suas respostas. As seguintes perguntas ajudarão você a decidir de que tipo de destruidor você precisa:

  1. Sua classe deve ser usada como classe base?
    • Não: Declare o destruidor público não virtual para evitar o apontador v em cada objeto da classe * .
    • Sim: Leia a próxima pergunta.
  2. Sua classe base é abstrata? (ou seja, algum método puro virtual?)
    • Não: tente tornar sua classe base abstrata redesenhando sua hierarquia de classes
    • Sim: Leia a próxima pergunta.
  3. Deseja permitir a exclusão polimórfica através de um ponteiro de base?
    • Não: Declare o destruidor virtual protegido para impedir o uso indesejado.
    • Sim: declarar destruidor virtual público (sem sobrecarga neste caso).

Eu espero que isso ajude.

* É importante observar que no C ++ não há como marcar uma classe como final (ou seja, não subclassável), portanto, caso decida declarar seu destruidor não virtual e público, lembre-se de alertar explicitamente seus colegas programadores contra derivado de sua classe.

Referências:

davidag
fonte
11
Esta resposta está parcialmente desatualizada, agora existe uma palavra-chave final em C ++.
Étienne
10

Sim, é sempre importante. Classes derivadas podem alocar memória ou manter referência a outros recursos que precisarão ser limpos quando o objeto for destruído. Se você não atribuir destruidores virtuais de suas interfaces / classes abstratas, toda vez que você excluir uma instância de classe derivada por meio de um identificador de classe base, o destruidor de sua classe derivada não será chamado.

Portanto, você está abrindo o potencial de vazamentos de memória

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
JO.
fonte
Na verdade, na verdade nesse exemplo, não pode vazar memória apenas, mas possivelmente falhar: - /
Evan Teran
7

Nem sempre é necessário, mas acho que é uma boa prática. O que ele faz é que ele permite que um objeto derivado seja excluído com segurança por meio de um ponteiro do tipo base.

Então, por exemplo:

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

está mal formado se Basenão tiver um destruidor virtual, porque tentará excluir o objeto como se fosse um Base *.

Evan Teran
fonte
você não deseja corrigir boost :: shared_pointer p (new Derived) para se parecer com boost :: shared_pointer <Base> p (new Derived); ? talvez as pessoas entendam a sua resposta e votem
Johannes Schaub - litb 29/11
EDIT: "Codificou" algumas partes para tornar os colchetes angulares visíveis, como sugerido no texto.
j_random_hacker
@EvanTeran: Não tenho certeza se isso mudou desde que a resposta foi publicada originalmente (a documentação do Boost em boost.org/doc/libs/1_52_0/libs/smart_ptr/shared_ptr.htm sugere que pode ter), mas não é verdade nos dias de hoje que shared_ptrtentará excluir o objeto como se fosse um Base *- ele se lembra do tipo de coisa com a qual você o criou. Consulte o link referenciado, em particular o bit que diz "O destruidor chamará delete com o mesmo ponteiro, completo com seu tipo original, mesmo quando T não tiver um destruidor virtual ou for nulo".
Stuart Golodetz
@StuartGolodetz: Hmm, você pode estar certo, mas sinceramente não tenho certeza. Ainda pode estar mal formado neste contexto devido à falta de destruidor virtual. Vale a pena investigar.
precisa
@EvanTeran: Caso seja útil - stackoverflow.com/questions/3899790/shared-ptr-magic .
Stuart Golodetz
5

Não é apenas uma boa prática. É a regra nº 1 para qualquer hierarquia de classes.

  1. A classe mais básica de uma hierarquia em C ++ deve ter um destruidor virtual

Agora, o porquê. Tome a hierarquia animal típica. Os destruidores virtuais passam pelo despacho virtual, assim como qualquer outra chamada de método. Veja o exemplo a seguir.

Animal* pAnimal = GetAnimal();
delete pAnimal;

Suponha que Animal é uma classe abstrata. A única maneira pela qual o C ++ conhece o destruidor adequado a ser chamado é através do envio de método virtual. Se o destruidor não for virtual, ele simplesmente chamará o destruidor de Animal e não destruirá nenhum objeto nas classes derivadas.

A razão para tornar o destruidor virtual na classe base é que ele simplesmente remove a escolha das classes derivadas. Seu destruidor se torna virtual por padrão.

JaredPar
fonte
2
Eu concordo principalmente com você, porque geralmente, ao definir uma hierarquia, você pode se referir a um objeto derivado usando um ponteiro / referência de classe base. Mas nem sempre é esse o caso, e nesses outros casos, pode ser suficiente proteger o dtor da classe base.
j_random_hacker
@j_random_hacker tornando-se protegido não irá protegê-lo de exclusões internos incorretos
JaredPar
1
@JaredPar: Isso mesmo, mas pelo menos você pode ser responsável em seu próprio código - o difícil é garantir que o código do cliente não possa causar a explosão do código. (Da mesma forma, fazendo uma privada membro de dados não impede código interno de fazer algo estúpido com esse membro.)
j_random_hacker
@j_random_hacker, desculpe-me por responder com uma postagem no blog, mas realmente se encaixa nesse cenário. blogs.msdn.com/jaredpar/archive/2008/03/24/…
JaredPar 10/02/09
@ JaredPar: Excelente publicação, concordo 100% com você, principalmente sobre a verificação de contratos no código de varejo. Só quero dizer que há casos em que você sabe que não precisa de um dtor virtual. Exemplo: classes de tag para envio de modelo. Eles têm tamanho 0, você só usa herança para indicar especializações.
j_random_hacker
3

A resposta é simples: você precisa que ela seja virtual, caso contrário, a classe base não seria uma classe polimórfica completa.

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

Você prefere a exclusão acima, mas se o destruidor da classe base não for virtual, apenas o destruidor da classe base será chamado e todos os dados na classe derivada permanecerão desmarcados.

fatma.ekici
fonte