Perché dovrei dichiarare un distruttore virtuale per una classe astratta in C ++?


165

So che è una buona pratica dichiarare i distruttori virtuali per le classi di base in C ++, ma è sempre importante dichiarare i virtualdistruttori anche per le classi astratte che funzionano come interfacce? Fornisci alcuni motivi ed esempi del perché.

Risposte:


196

È ancora più importante per un'interfaccia. Qualsiasi utente della tua classe probabilmente terrà un puntatore all'interfaccia, non un puntatore all'implementazione concreta. Quando arrivano per eliminarlo, se il distruttore non è virtuale, chiamano il distruttore dell'interfaccia (o il predefinito fornito dal compilatore, se non ne hai specificato uno), non il distruttore della classe derivata. Perdita di memoria istantanea.

Per esempio

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 pinvoca un comportamento indefinito. Non è garantito chiamare Interface::~Interface.
Mankarse,

@Mankarse: puoi spiegare cosa lo rende indefinito? Se Derived non implementasse il proprio distruttore, sarebbe comunque un comportamento indefinito?
Ponkadoodle,

14
@Wallacoloo: Non è definito a causa di [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. .... Sarebbe ancora indefinito se Derived usasse un distruttore generato implicitamente.
Mankarse,

37

La risposta alla tua domanda è spesso, ma non sempre. Se la tua classe astratta vieta ai clienti di chiamare delete su un puntatore (o se lo dice nella sua documentazione), sei libero di non dichiarare un distruttore virtuale.

Puoi vietare ai client di chiamare delete su un puntatore ad esso proteggendo il suo distruttore. Funzionando in questo modo, è perfettamente sicuro e ragionevole omettere un distruttore virtuale.

Alla fine finirai con nessuna tabella dei metodi virtuali e finirai per segnalare ai tuoi clienti la tua intenzione di renderlo non cancellabile tramite un puntatore ad esso, quindi hai davvero motivo di non dichiararlo virtuale in quei casi.

[Vedi l'articolo 4 in questo articolo: http://www.gotw.ca/publications/mill18.htm ]


La chiave per far funzionare la tua risposta è "su cui l'eliminazione non è richiesta." In genere se si dispone di una classe base astratta progettata per essere un'interfaccia, verrà eliminata la classe interfaccia.
John Dibling,

Come ha sottolineato John sopra, quello che stai suggerendo è piuttosto pericoloso. Stai facendo affidamento sul presupposto che i client della tua interfaccia non distruggeranno mai un oggetto conoscendo solo il tipo di base. L'unico modo in cui potresti garantire che, se non virtuale, è proteggere il dtor della classe astratta.
Michel,

Michel, l'ho detto :) "Se lo fai, rendi il tuo distruttore protetto. Se lo fai, i client non saranno in grado di eliminare usando un puntatore a quell'interfaccia." e in effetti non si basa sui clienti, ma deve imporlo dicendo ai clienti "non si può fare ...". Non vedo alcun pericolo
Johannes Schaub - litb,

ho corretto la povera formulazione della mia risposta ora. lo afferma esplicitamente ora che non si basa sui client. in realtà ho pensato che fosse ovvio che fare affidamento sul fatto che i clienti facessero qualcosa è fuori strada comunque. grazie :)
Johannes Schaub - litb

2
+1 per menzionare i distruttori protetti, che sono l'altra "via d'uscita" dal problema di chiamare accidentalmente il distruttore sbagliato quando si elimina un puntatore a una classe base.
j_random_hacker,

23

Ho deciso di fare qualche ricerca e provare a sintetizzare le tue risposte. Le seguenti domande ti aiuteranno a decidere di quale tipo di distruttore hai bisogno:

  1. La tua classe è pensata per essere usata come classe base?
    • No: dichiara distruttore pubblico non virtuale per evitare il puntatore a v su ogni oggetto della classe * .
    • Sì: leggi la prossima domanda.
  2. La tua classe di base è astratta? (cioè qualsiasi metodo puro virtuale?)
    • No: prova a rendere astratta la tua classe di base ridisegnando la gerarchia delle classi
    • Sì: leggi la prossima domanda.
  3. Vuoi consentire la cancellazione polimorfica attraverso un puntatore di base?
    • No: dichiarare distruttore virtuale protetto per impedire l'utilizzo indesiderato.
    • Sì: dichiara il distruttore virtuale pubblico (in questo caso nessun sovraccarico).

Spero che aiuti.

* È importante notare che in C ++ non c'è modo di contrassegnare una classe come finale (cioè non sottoclassabile), quindi nel caso in cui decidi di dichiarare il tuo distruttore non virtuale e pubblico, ricorda di mettere in guardia esplicitamente i tuoi colleghi programmatori contro derivante dalla tua classe.

Riferimenti:


11
Questa risposta è in parte obsoleta, ora c'è una parola chiave finale in C ++.
Étienne,

10

Sì, è sempre importante. Le classi derivate possono allocare memoria o contenere riferimenti ad altre risorse che dovranno essere ripulite quando l'oggetto viene distrutto. Se non si assegnano distruttori virtuali alle interfacce / classi astratte, ogni volta che si elimina un'istanza di classe derivata tramite un handle di classe base, il distruttore della classe derivata non verrà chiamato.

Quindi, stai aprendo il potenziale per perdite di memoria

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

È vero, in effetti in quell'esempio, potrebbe non essere solo una perdita di memoria, ma forse un arresto anomalo: - /
Evan Teran

7

Non è sempre necessario, ma trovo che sia una buona pratica. Ciò che fa è che consente a un oggetto derivato di essere eliminato in modo sicuro tramite un puntatore di un tipo base.

Quindi per esempio:

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

è mal formato se Basenon ha un distruttore virtuale, perché tenterà di eliminare l'oggetto come se fosse un Base *.


non vuoi correggere boost :: shared_pointer p (new Derived) in modo che assomigli a boost :: shared_pointer <Base> p (new Derived); ? forse ppl capirà allora la tua risposta e voterà
Johannes Schaub - lettb

EDIT: "Codificato" un paio di parti per rendere visibili le parentesi angolari, come suggerito da litb.
j_random_hacker,

@EvanTeran: Non sono sicuro che sia cambiato da quando la risposta è stata originariamente pubblicata (la documentazione di Boost su boost.org/doc/libs/1_52_0/libs/smart_ptr/shared_ptr.htm suggerisce che potrebbe essere), ma non è vero in questi giorni che shared_ptrtenterà di eliminare l'oggetto come se fosse un Base *- ricorda il tipo di cosa con cui lo hai creato. Vedi il link di riferimento, in particolare il bit che dice "Il distruttore chiamerà delete con lo stesso puntatore, completo del suo tipo originale, anche quando T non ha un distruttore virtuale o è nullo".
Stuart Golodetz,

@StuartGolodetz: Hmm, potresti avere ragione, ma sinceramente non ne sono sicuro. Potrebbe ancora essere mal formato in questo contesto a causa della mancanza di distruttore virtuale. Vale la pena esaminarlo.
Evan Teran,


5

Non è solo una buona pratica. È la regola n. 1 per qualsiasi gerarchia di classi.

  1. La maggior parte della classe di base di una gerarchia in C ++ deve avere un distruttore virtuale

Ora per il perché. Prendi la tipica gerarchia animale. I distruttori virtuali passano attraverso l'invio virtuale proprio come qualsiasi altra chiamata di metodo. Prendi il seguente esempio.

Animal* pAnimal = GetAnimal();
delete pAnimal;

Supponiamo che Animal sia una classe astratta. L'unico modo in cui C ++ conosce il distruttore corretto da chiamare è tramite l'invio di metodi virtuali. Se il distruttore non è virtuale, chiamerà semplicemente il distruttore di animali e non distruggerà alcun oggetto nelle classi derivate.

Il motivo per rendere virtuale il distruttore nella classe base è che rimuove semplicemente la scelta dalle classi derivate. Il loro distruttore diventa virtuale per impostazione predefinita.


2
Sono principalmente d'accordo con te, perché di solito quando si definisce una gerarchia si desidera poter fare riferimento a un oggetto derivato utilizzando un puntatore / riferimento di classe base. Ma non è sempre così, e in quegli altri casi, potrebbe essere sufficiente proteggere il dtor della classe base.
j_random_hacker,

@j_random_hacker renderlo protetto non ti proteggerà da cancellazioni interne errate
JaredPar,

1
@JaredPar: Esatto, ma almeno puoi essere responsabile nel tuo codice - la cosa difficile è assicurarsi che il codice client non possa far esplodere il tuo codice. (Allo stesso modo, rendere privato un membro di dati non impedisce al codice interno di fare qualcosa di stupido con quel membro.)
j_random_hacker,

@j_random_hacker, mi dispiace rispondere con un post sul blog ma si adatta davvero a questo scenario. blogs.msdn.com/jaredpar/archive/2008/03/24/…
JaredPar,

@JaredPar: eccellente post, sono d'accordo con te al 100%, in particolare riguardo al controllo dei contratti nel codice di vendita. Intendo solo che ci sono casi in cui sai che non hai bisogno di un dtor virtuale. Esempio: classi di tag per l'invio del modello. Hanno dimensioni 0, usi l'ereditarietà solo per indicare le specializzazioni.
j_random_hacker,

3

La risposta è semplice, è necessario che sia virtuale, altrimenti la classe base non sarebbe una classe polimorfica completa.

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

Preferiresti la cancellazione di cui sopra, ma se il distruttore della classe base non è virtuale, verrà chiamato solo il distruttore della classe base e tutti i dati nella classe derivata rimarranno non cancellati.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.