C'è mai una buona ragione per non dichiarare un distruttore virtuale per una classe? Quando dovresti evitare specificamente di scriverne uno?
C'è mai una buona ragione per non dichiarare un distruttore virtuale per una classe? Quando dovresti evitare specificamente di scriverne uno?
Risposte:
Non è necessario utilizzare un distruttore virtuale quando una delle seguenti condizioni è vera:
Nessun motivo specifico per evitarlo a meno che tu non sia davvero così a corto di memoria.
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 =default
significherà 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
}
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.
Un distruttore virtuale è necessario ogni volta che esiste la possibilità che delete
possa 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 B
sia 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 delete
molti 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.
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.
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.
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!"
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.
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.
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.
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 .
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.