Quando non dovresti usare i distruttori virtuali?


Risposte:


72

Non è necessario utilizzare un distruttore virtuale quando una delle seguenti condizioni è vera:

  • Nessuna intenzione di derivarne classi
  • Nessuna istanza sul mucchio
  • Nessuna intenzione di memorizzare in un puntatore di una superclasse

Nessun motivo specifico per evitarlo a meno che tu non sia davvero così a corto di memoria.


25
Questa non è una buona risposta. "Non c'è bisogno" è diverso da "non dovrebbe", e "nessuna intenzione" è diverso da "reso impossibile".
Programmatore Windows

5
Aggiungere anche: nessuna intenzione di eliminare un'istanza tramite un puntatore di classe base.
Adam Rosenfield,

9
Questo non risponde davvero alla domanda. Dov'è la tua buona ragione per non usare un dtor virtuale?
mxcl

9
Penso che quando non c'è bisogno di fare qualcosa, questo è un buon motivo per non farlo. Segue il principio del design semplice di XP.
settembre

12
Dicendo che "non hai intenzione", stai facendo un'enorme ipotesi su come verrà utilizzata la tua classe. Mi sembra che la soluzione più semplice nella maggior parte dei casi (che dovrebbe quindi essere l'impostazione predefinita) dovrebbe essere quella di avere distruttori virtuali, ed evitarli solo se hai un motivo specifico per non farlo. Quindi sono ancora curioso di sapere quale sarebbe una buona ragione.
ckarras

68

Per rispondere esplicitamente alla domanda, cioè quando non dichiarare un distruttore virtuale.

C ++ '98 / '03

L'aggiunta di un distruttore virtuale potrebbe cambiare la tua classe da POD (semplici vecchi dati) * o aggregata a non-POD. Ciò può impedire la compilazione del progetto se il tipo di classe è inizializzato aggregato da qualche parte.

struct A {
  // virtual ~A ();
  int i;
  int j;
};
void foo () { 
  A a = { 0, 1 };  // Will fail if virtual dtor declared
}

In un caso estremo, una tale modifica può anche causare un comportamento indefinito in cui la classe viene utilizzata in un modo che richiede un POD, ad esempio passandola tramite un parametro di ellissi o usandola con memcpy.

void bar (...);
void foo (A & a) { 
  bar (a);  // Undefined behavior if virtual dtor declared
}

[* Un tipo POD è un tipo che ha garanzie specifiche sul layout della memoria. Lo standard in realtà dice solo che se dovessi copiare da un oggetto con tipo POD in un array di caratteri (o caratteri non firmati) e viceversa, il risultato sarà lo stesso dell'oggetto originale.]

C ++ moderno

Nelle recenti versioni di C ++, il concetto di POD era diviso tra il layout della classe e la sua costruzione, copia e distruzione.

Per il caso dei puntini di sospensione, non è più un comportamento indefinito, ora è supportato in modo condizionale con la semantica definita dall'implementazione (N3937 - ~ C ++ '14 - 5.2.2 / 7):

... Il passaggio di un argomento potenzialmente valutato di tipo classe (clausola 9) con un costruttore di copie non banale, un costruttore di spostamenti non banale o un distruttore banale, senza parametro corrispondente, è supportato in modo condizionale con l'implementazione- semantica definita.

Dichiarare un distruttore diverso da =defaultsignificherà che non è banale (12.4 / 5)

... Un distruttore è banale se non è fornito dall'utente ...

Altre modifiche al C ++ moderno riducono l'impatto del problema di inizializzazione aggregata poiché è possibile aggiungere un costruttore:

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

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

1
Hai ragione, e io avevo torto, la performance non è l'unica ragione. Ma questo mostra che avevo ragione sul resto: il programmatore della classe farebbe meglio a includere il codice per evitare che la classe venga mai ereditata da qualcun altro.
Programmatore Windows

caro Richard, puoi per favore commentare un po 'di più su quello che hai scritto. Non capisco il tuo punto, ma mi sembra l'unico punto prezioso che ho trovato su Google) O forse puoi dare un link a una spiegazione più dettagliata?
John Smith,

1
@ JohnSmith ho aggiornato la risposta. Si spera che questo aiuti.
Richard Corden

28

Dichiaro un distruttore virtuale se e solo se ho metodi virtuali. Una volta che ho metodi virtuali, non mi fido di me stesso per evitare di crearne un'istanza sull'heap o di memorizzare un puntatore alla classe base. Entrambe sono operazioni estremamente comuni e spesso perdono risorse silenziosamente se il distruttore non viene dichiarato virtuale.


3
E, infatti, c'è un'opzione di avviso su gcc che avverte proprio in quel caso (metodi virtuali ma nessun dtor virtuale).
CesarB

6
Non corri quindi il rischio di perdere memoria se derivi dalla classe, indipendentemente dal fatto che tu abbia altre funzioni virtuali?
Mag Roader

1
Sono d'accordo con mag. Questo utilizzo di un distruttore virtuale e / o di un metodo virtuale sono requisiti separati. Il distruttore virtuale consente a una classe di eseguire la pulizia (ad esempio, eliminare la memoria, chiudere i file, ecc.) E garantisce anche che i costruttori di tutti i suoi membri vengano chiamati.
user48956

7

Un distruttore virtuale è necessario ogni volta che esiste la possibilità che deletepossa essere chiamato su un puntatore a un oggetto di una sottoclasse con il tipo della propria classe. Questo assicura che il distruttore corretto venga chiamato in fase di esecuzione senza che il compilatore debba conoscere la classe di un oggetto sull'heap in fase di compilazione. Ad esempio, supponiamo che Bsia una sottoclasse di A:

A *x = new B;
delete x;     // ~B() called, even though x has type A*

Se il tuo codice non è critico per le prestazioni, sarebbe ragionevole aggiungere un distruttore virtuale a ogni classe base che scrivi, solo per sicurezza.

Tuttavia, se ti ritrovi a inserire deletemolti oggetti in un ciclo ristretto, il sovraccarico delle prestazioni di chiamare una funzione virtuale (anche una vuota) potrebbe essere evidente. Il compilatore di solito non può incorporare queste chiamate e il processore potrebbe avere difficoltà a prevedere dove andare. È improbabile che ciò abbia un impatto significativo sulle prestazioni, ma vale la pena menzionarlo.


"Se il tuo codice non è critico per le prestazioni, sarebbe ragionevole aggiungere un distruttore virtuale a ogni classe di base che scrivi, solo per sicurezza." dovrebbe essere enfatizzato maggiormente in ogni risposta che vedo
csguy

5

Le funzioni virtuali significano che ogni oggetto allocato aumenta il costo della memoria da un puntatore alla tabella delle funzioni virtuali.

Quindi, se il tuo programma prevede l'allocazione di un numero molto elevato di oggetti, varrebbe la pena evitare tutte le funzioni virtuali per salvare i 32 bit aggiuntivi per oggetto.

In tutti gli altri casi, ti risparmierai la miseria del debug per rendere virtuale il dtor.


1
Solo pignoleria, ma oggigiorno un puntatore sarà spesso 64 bit invece di 32.
Head Geek

5

Non tutte le classi C ++ sono adatte per l'uso come classe base con polimorfismo dinamico.

Se vuoi che la tua classe sia adatta al polimorfismo dinamico, il suo distruttore deve essere virtuale. Inoltre, qualsiasi metodo che una sottoclasse potrebbe plausibilmente voler sovrascrivere (il che potrebbe significare tutti i metodi pubblici, più potenzialmente alcuni metodi protetti usati internamente) deve essere virtuale.

Se la tua classe non è adatta al polimorfismo dinamico, il distruttore non dovrebbe essere contrassegnato come virtuale, perché farlo è fuorviante. Incoraggia semplicemente le persone a usare la tua classe in modo errato.

Ecco un esempio di una classe che non sarebbe adatta al polimorfismo dinamico, anche se il suo distruttore fosse virtuale:

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);
};

Il punto centrale di questa lezione è sedersi sulla pila per RAII. Se stai passando puntatori a oggetti di questa classe, figuriamoci sottoclassi di essa, allora stai facendo qualcosa di sbagliato.


2
L'uso polimorfico non implica la cancellazione polimorfica. Ci sono molti casi d'uso per una classe che ha metodi virtuali ma nessun distruttore virtuale. Considera una tipica finestra di dialogo definita staticamente, praticamente in qualsiasi toolkit GUI. La finestra genitore distruggerà gli oggetti figli e conosce il tipo esatto di ciascuno, ma anche tutte le finestre figlie verranno utilizzate polimorficamente in qualsiasi numero di posti, come hit testing, disegno, API di accessibilità che recuperano il testo per il testo- motori di sintesi vocale, ecc.
Ben Voigt

4
Vero, ma l'interrogante sta chiedendo quando dovresti evitare specificamente un distruttore virtuale. Per la finestra di dialogo che descrivi, un distruttore virtuale è inutile, ma IMO non è dannoso. Non sono sicuro di essere sicuro che non avrò mai bisogno di eliminare una finestra di dialogo utilizzando un puntatore di classe base, ad esempio potrei in futuro volere che la mia finestra genitore crei i suoi oggetti figlio utilizzando le factory. Quindi non si tratta di evitare il distruttore virtuale, solo che potresti non preoccuparti di averne uno. Tuttavia, un distruttore virtuale su una classe non adatta alla derivazione è dannoso perché fuorviante.
Steve Jessop

4

Una buona ragione per non dichiarare un distruttore come virtuale è quando questo evita che la tua classe abbia aggiunto una tabella di funzioni virtuali, e dovresti evitarlo quando possibile.

So che molte persone preferiscono dichiarare sempre i distruttori come virtuali, solo per stare sul sicuro. Ma se la tua classe non ha altre funzioni virtuali, non ha davvero senso avere un distruttore virtuale. Anche se dai la tua classe ad altre persone che poi ne derivano altre classi, non avrebbero motivo di chiamare delete su un puntatore che è stato trasmesso alla tua classe - e se lo facessero, lo considererei un bug.

Ok, c'è una sola eccezione, vale a dire se la tua classe è (male) usata per eseguire la cancellazione polimorfica di oggetti derivati, ma allora tu - o gli altri ragazzi - si spera che sappiate che questo richiede un distruttore virtuale.

In altre parole, se la tua classe ha un distruttore non virtuale, questa è un'affermazione molto chiara: "Non usarmi per eliminare oggetti derivati!"


3

Se hai una classe molto piccola con un numero enorme di istanze, il sovraccarico di un puntatore vtable può fare la differenza nell'utilizzo della memoria del tuo programma. Finché la tua classe non ha altri metodi virtuali, rendere il distruttore non virtuale salverà tale sovraccarico.


1

Di solito dichiaro il distruttore virtuale, ma se hai un codice critico per le prestazioni che viene utilizzato in un ciclo interno, potresti voler evitare la ricerca della tabella virtuale. Ciò può essere importante in alcuni casi, come il controllo delle collisioni. Ma fai attenzione a come distruggi quegli oggetti se usi l'ereditarietà, o distruggerai solo metà dell'oggetto.

Notare che la ricerca nella tabella virtuale avviene per un oggetto se un metodo su quell'oggetto è virtuale. Quindi non ha senso rimuovere la specifica virtuale su un distruttore se hai altri metodi virtuali nella classe.


1

Se devi assolutamente assicurarti che la tua classe non abbia una vtable, allora non devi avere anche un distruttore virtuale.

Questo è un caso raro, ma succede.

L'esempio più familiare di un pattern che fa questo sono le classi DirectX D3DVECTOR e D3DMATRIX. Questi sono metodi di classe invece di funzioni per lo zucchero sintattico, ma le classi intenzionalmente non hanno una tabella v per evitare il sovraccarico della funzione perché queste classi sono specificamente utilizzate nel ciclo interno di molte applicazioni ad alte prestazioni.


0

L'operazione che verrà eseguita sulla classe base, e che dovrebbe comportarsi virtualmente, dovrebbe essere virtuale. Se l'eliminazione può essere eseguita polimorficamente tramite l'interfaccia della classe base, allora deve comportarsi virtualmente ed essere virtuale.

Il distruttore non ha bisogno di essere virtuale se non si intende derivare dalla classe. E anche se lo fai, un distruttore non virtuale protetto è altrettanto valido se non è richiesta l'eliminazione dei puntatori della classe base .


-7

La risposta sulla performance è l'unica che io sappia che abbia una possibilità di essere vera. Se hai misurato e scoperto che la de-virtualizzazione dei distruttori velocizza davvero le cose, allora probabilmente hai altre cose in quella classe che devono essere accelerate, ma a questo punto ci sono considerazioni più importanti. Un giorno qualcuno scoprirà che il tuo codice fornirà loro una bella classe base e gli farà risparmiare una settimana di lavoro. Faresti meglio ad assicurarti che facciano il lavoro di quella settimana, copiando e incollando il tuo codice, invece di usare il tuo codice come base. Faresti meglio ad assicurarti di rendere privati ​​alcuni dei tuoi metodi importanti in modo che nessuno possa mai ereditare da te.


Il polimorfismo rallenterà sicuramente le cose. Confrontalo con una situazione in cui abbiamo bisogno del polimorfismo e scegli di non farlo, sarà ancora più lento. Esempio: implementiamo tutta la logica nel distruttore della classe base, utilizzando RTTI e un'istruzione switch per ripulire le risorse.
settembre

1
In C ++, non è tua responsabilità impedirmi di ereditare dalle tue classi che hai documentato non sono adatte per l'uso come classi base. È mia responsabilità usare l'eredità con cautela. A meno che la guida allo stile della casa non dica diversamente, ovviamente.
Steve Jessop,

1
... il solo fatto di rendere virtuale il distruttore non significa che la classe funzionerà necessariamente correttamente come classe base. Quindi contrassegnarlo come virtuale "solo perché", invece di fare quella valutazione, è scrivere un assegno che il mio codice non può incassare.
Steve Jessop
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.