Domanda

C'è mai una buona ragione per non dichiarare un distruttore virtuale per una classe? Quando dovresti evitare di scriverne uno in particolare?

È stato utile?

Soluzione

Non è necessario utilizzare un distruttore virtuale in presenza di una delle condizioni seguenti:

  • Nessuna intenzione di derivarne classi
  • Nessuna istanza nell'heap
  • Nessuna intenzione di memorizzare in un puntatore di una superclasse

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

Altri suggerimenti

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 dall'essere POD (semplici vecchi dati) * o aggregato a non-POD. Ciò può impedire la compilazione del progetto se il tipo di classe è inizializzato 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, tale modifica può anche causare un comportamento indefinito in cui la classe viene utilizzata in un modo che richiede un POD, ad es. passando attraverso un parametro con puntini di sospensione o utilizzandolo 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 suo layout di memoria. Lo standard dice solo che se dovessi copiare da un oggetto con tipo POD in una matrice 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 è stato diviso tra il layout della classe e la sua costruzione, copia e distruzione.

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

  

... Il passaggio di un argomento potenzialmente valutato del tipo di classe (clausola 9) con un costruttore di copie non banale, un costruttore di movimento non banale o un distruttore on-banale, senza parametro corrispondente, è supportato condizionatamente con semantica definita dall'implementazione.

Dichiarare un distruttore diverso da = default significa che non è banale (12.4 / 5)

  

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

Altre modifiche a Modern C ++ 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 creare un'istanza nell'heap o di archiviare un puntatore alla classe base. Entrambe sono operazioni estremamente comuni e spesso perdono risorse silenziosamente se il distruttore non viene dichiarato virtuale.

È necessario un distruttore virtuale ogni volta che c'è la possibilità che elimina possa essere chiamato su un puntatore a un oggetto di una sottoclasse con il tipo della tua classe. Questo si 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 di base che scrivi, solo per sicurezza.

Tuttavia, se ti sei trovato delete in molti oggetti in un ciclo stretto, il sovraccarico prestazionale di chiamare una funzione virtuale (anche uno vuoto) 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 indicano che ogni oggetto allocato aumenta il costo della memoria da un puntatore della tabella delle funzioni virtuali.

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

In tutti gli altri casi, ti salverai la miseria di 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, allora il suo distruttore deve essere virtuale. Inoltre, tutti i metodi che una sottoclasse potrebbe presumibilmente voler sovrascrivere (il che potrebbe significare tutti i metodi pubblici, oltre potenzialmente quelli protetti utilizzati internamente) devono essere virtuali.

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 utilizzare la 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 classe è sedersi in pila per RAII. Se stai passando puntatori a oggetti di questa classe, per non parlare delle sottoclassi di essa, allora stai facendo qualcosa di sbagliato.

Un buon motivo per non dichiarare un distruttore come virtuale è quando questo salva la tua classe dall'aggiunta di una tabella di funzione virtuale, e dovresti evitarlo quando possibile.

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

Va ??bene, c'è una sola eccezione, vale a dire se la tua classe è (usata male) per eseguire la cancellazione polimorfica di oggetti derivati, ma poi tu o gli altri ragazzi speriamo che questo richieda un distruttore virtuale.

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

Se hai una classe molto piccola con un numero enorme di istanze, l'overhead 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à quel sovraccarico.

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

Nota che la ricerca della tabella virtuale avviene per un oggetto se il metodo qualsiasi 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, non devi avere anche un distruttore virtuale.

Questo è un caso raro, ma succede.

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

All'operazione che verrà eseguita sulla classe base e che dovrebbe comportarsi virtualmente, dovrebbe essere virtuale. Se la cancellazione può essere eseguita polimorficamente attraverso 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 la cancellazione dei puntatori della classe base .

La risposta alla performance è l'unica che io conosca che ha la possibilità di essere vera. Se hai misurato e scoperto che la de-virtualizzazione dei tuoi distruttori accelera davvero le cose, allora probabilmente hai altre cose in quella classe che hanno bisogno di accelerare, ma a questo punto ci sono considerazioni più importanti. Un giorno qualcuno scoprirà che il tuo codice fornirebbe una buona classe base per loro e gli risparmierebbe una settimana di lavoro. Faresti meglio a assicurarti che facciano quel lavoro di quella settimana, copiando e incollando il tuo codice, invece di usarlo come base. Faresti meglio a rendere privati ??alcuni dei tuoi metodi importanti in modo che nessuno possa mai ereditare da te.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top