Qual è il costo dell'utilizzo dei distruttori virtuali se lo uso anche se non è necessario?
Il costo dell'introduzione di una funzione virtuale in una classe (ereditata o parte della definizione della classe) è un costo iniziale probabilmente molto elevato (o non dipendente dall'oggetto) di un puntatore virtuale memorizzato per oggetto, in questo modo:
struct Integer
{
virtual ~Integer() {}
int value;
};
In questo caso, il costo della memoria è relativamente enorme. La dimensione effettiva della memoria di un'istanza di classe ora apparirà spesso così su architetture a 64 bit:
struct Integer
{
// 8 byte vptr overhead
int value; // 4 bytes
// typically 4 more bytes of padding for alignment of vptr
};
Il totale è di 16 byte per questa Integer
classe anziché solo 4 byte. Se ne memorizzassimo un milione in un array, finiremmo con 16 megabyte di utilizzo della memoria: il doppio delle dimensioni della tipica cache della CPU L3 da 8 MB e iterare ripetutamente attraverso tale array può essere molte volte più lento dell'equivalente di 4 megabyte senza il puntatore virtuale a causa di ulteriori errori nella cache e errori di pagina.
Questo costo del puntatore virtuale per oggetto, tuttavia, non aumenta con più funzioni virtuali. Puoi avere 100 funzioni di membro virtuale in una classe e l'overhead per istanza sarebbe comunque un singolo puntatore virtuale.
Il puntatore virtuale è in genere la preoccupazione più immediata dal punto di vista ambientale. Tuttavia, oltre a un puntatore virtuale per istanza è previsto un costo per classe. Ogni classe con funzioni virtuali genera vtable
in memoria un archivio che memorizza gli indirizzi per le funzioni che dovrebbe effettivamente chiamare (invio virtuale / dinamico) quando viene effettuata una chiamata di funzione virtuale. L' vptr
archiviazione per istanza quindi punta a questo specifico della classe vtable
. Questo sovraccarico di solito è una preoccupazione minore, ma potrebbe gonfiare la dimensione binaria e aggiungere un po 'di costi di runtime se questo sovraccarico è stato inutilmente pagato per mille classi in una base di codice complessa, ad es. Questo vtable
lato del costo aumenta effettivamente proporzionalmente con più e più funzioni virtuali nel mix.
Gli sviluppatori Java che lavorano in aree critiche per le prestazioni comprendono molto bene questo tipo di overhead (anche se spesso descritto nel contesto del boxing), poiché un tipo definito dall'utente Java eredita implicitamente da una object
classe base centrale e tutte le funzioni in Java sono implicitamente virtuali (sostituibili ) in natura, salvo diversa indicazione. Di conseguenza, un Java Integer
tende anche a richiedere 16 byte di memoria su piattaforme a 64 bit come risultato di questi vptr
metadati di stile associati per istanza, ed è generalmente impossibile in Java avvolgere qualcosa come un singolo int
in una classe senza pagare un runtime costo delle prestazioni per questo.
Quindi la domanda è: perché c ++ non rende virtuali tutti i distruttori per impostazione predefinita?
Il C ++ privilegia davvero le prestazioni con un tipo di mentalità "pay as you go" e anche molti progetti basati su hardware bare metal ereditati da C. Non vuole includere inutilmente l'overhead richiesto per la generazione di vtable e il dispacciamento dinamico per ogni singola classe / istanza coinvolta. Se le prestazioni non sono uno dei motivi principali per cui stai usando un linguaggio come C ++, potresti trarre maggiori benefici da altri linguaggi di programmazione là fuori in quanto gran parte del linguaggio C ++ è meno sicuro e più difficile di quanto idealmente potrebbe essere con prestazioni spesso il motivo chiave per favorire tale design.
Quando NON devo usare i distruttori virtuali?
Abbastanza spesso. Se una classe non è progettata per essere ereditata, allora non ha bisogno di un distruttore virtuale e finirebbe solo per pagare un sovraccarico possibilmente grande per qualcosa di cui non ha bisogno. Allo stesso modo, anche se una classe è progettata per essere ereditata ma non si eliminano mai le istanze di sottotipo tramite un puntatore di base, non richiede nemmeno un distruttore virtuale. In tal caso, una pratica sicura è quella di definire un distruttore non virtuale protetto, in questo modo:
class BaseClass
{
protected:
// Disallow deleting/destroying subclass objects through `BaseClass*`.
~BaseClass() {}
};
In tal caso NON dovrei usare i distruttori virtuali?
In realtà è più facile coprire quando dovresti usare i distruttori virtuali. Molto spesso molte più classi nella tua base di codice non saranno progettate per l'ereditarietà.
std::vector
, ad esempio, non è progettato per essere ereditato e in genere non deve essere ereditato (design molto traballante), in quanto ciò sarà soggetto a questo problema di eliminazione del puntatore di base ( std::vector
evita deliberatamente un distruttore virtuale) oltre a problemi di suddivisione degli oggetti goffi se il tuo la classe derivata aggiunge qualsiasi nuovo stato.
In generale una classe ereditata dovrebbe avere un distruttore virtuale pubblico o uno protetto, non virtuale. Da C++ Coding Standards
, capitolo 50:
50. Rendi pubblici e virtuali i distruttori della classe base o protetti e non virtuali. Per cancellare o non cancellare; questa è la domanda: se è necessario consentire l'eliminazione tramite un puntatore a una Base di base, il distruttore di Base deve essere pubblico e virtuale. Altrimenti, dovrebbe essere protetto e non virtuale.
Una delle cose che il C ++ tende a enfatizzare implicitamente (perché i progetti tendono a diventare molto fragili e scomodi e forse anche non sicuri altrimenti) è l'idea che l'ereditarietà non sia un meccanismo progettato per essere utilizzato come ripensamento. È un meccanismo di estensibilità con in mente il polimorfismo, ma uno che richiede lungimiranza su dove è necessaria l'estensibilità. Di conseguenza, le classi di base dovrebbero essere progettate come radici di una gerarchia ereditaria in anticipo e non come qualcosa che erediti in seguito come ripensamento senza una tale previsione in anticipo.
Nei casi in cui si desidera semplicemente ereditare per riutilizzare il codice esistente, la composizione è spesso fortemente incoraggiata (principio di riutilizzo composito).