Quando NON usare i distruttori virtuali?


48

Credevo di aver cercato molte volte sui distruttori virtuali, la maggior parte menziona lo scopo dei distruttori virtuali e perché hai bisogno di distruttori virtuali. Inoltre, penso che nella maggior parte dei casi i distruttori debbano essere virtuali.

Quindi la domanda è: perché c ++ non rende virtuali tutti i distruttori per impostazione predefinita? o in altre domande:

Quando NON devo usare i distruttori virtuali?

In tal caso NON dovrei usare i distruttori virtuali?

Qual è il costo dell'utilizzo dei distruttori virtuali se lo uso anche se non è necessario?


6
E se la tua classe non dovesse essere ereditata? Guarda molte delle classi di libreria standard, alcune hanno funzioni virtuali perché non sono progettate per essere ereditate.
Qualche programmatore, amico,

4
Inoltre, penso che nella maggior parte dei casi i distruttori debbano essere virtuali. No. Affatto. Solo coloro che abusano dell'eredità (piuttosto che favorire la composizione) la pensano così. Ho visto intere applicazioni con solo una manciata di classi di base e funzioni virtuali.
Matthieu M.,

1
@underscore_d Con implementazioni tipiche, verrebbe generato un codice aggiuntivo per qualsiasi classe polimorfica a meno che tutte queste cose implicite non siano state devirtualizzate e ottimizzate. All'interno di ABI comuni, ciò comporta almeno una tabella per ogni classe. Anche il layout della classe deve essere modificato. Non puoi tornare in modo affidabile dopo aver pubblicato una classe come parti di alcune interfacce pubbliche, perché cambiarlo di nuovo si romperà la compatibilità ABI, dal momento che ovviamente è male (se mai possibile) aspettarsi la devirtualizzazione come contratti di interfaccia in generale.
FrankHB,

1
@underscore_d La frase "al momento della compilazione" non è precisa, ma penso che ciò significhi che un distruttore virtuale non può essere banale né specificato con constexpr, quindi la generazione di codice aggiuntivo è difficile da evitare (a meno che non si eviti del tutto la distruzione di tali oggetti) quindi danneggerebbe più o meno le prestazioni di runtime.
FrankHB,

2
@underscore_d Il "puntatore" sembra aringa rossa. Probabilmente dovrebbe essere un puntatore al membro (che non è un puntatore per definizione). Con i soliti ABI, un puntatore al membro spesso non si adatta a una parola macchina (come puntatori tipici), e cambiando una classe da non polimorfica a polimorfica si cambierebbe spesso la dimensione del puntatore a membro di questa classe.
FrankHB,

Risposte:


41

Se aggiungi un distruttore virtuale a una classe:

  • nella maggior parte (tutte?) delle attuali implementazioni C ++, ogni istanza di oggetto di quella classe deve archiviare un puntatore alla tabella di invio virtuale per il tipo di runtime e quella stessa tabella di invio virtuale aggiunta all'immagine eseguibile

  • l'indirizzo della tabella di invio virtuale non è necessariamente valido tra i processi, il che può impedire la condivisione sicura di tali oggetti nella memoria condivisa

  • avere un puntatore virtuale incorporato frustrare la creazione di una classe con layout di memoria che corrisponda a un formato di input o output noto (ad esempio, quindi Price_Tick*potrebbe essere diretto direttamente alla memoria opportunamente allineata in un pacchetto UDP in arrivo e utilizzato per analizzare / accedere o modificare i dati, oppure collocamento di newuna tale classe per scrivere dati in un pacchetto in uscita)

  • le chiamate del distruttore stesso possono - a determinate condizioni - essere inviate virtualmente e quindi fuori linea, mentre i distruttori non virtuali potrebbero essere allineati o ottimizzati se banali o irrilevanti per il chiamante

L'argomento "non progettato per essere ereditato da" non sarebbe una ragione pratica per non avere sempre un distruttore virtuale se non fosse anche peggio in modo pratico come spiegato sopra; ma dato che è peggio, questo è un criterio fondamentale per quando pagare il costo: impostazione predefinita per avere un distruttore virtuale se la tua classe deve essere usata come classe base . Questo non è sempre necessario, ma garantisce che le classi nell'erirarchia possano essere utilizzate più liberamente senza un comportamento indefinito accidentale se un distruttore di classe derivato viene invocato usando un puntatore o un riferimento di classe base.

"nella maggior parte dei casi i distruttori devono essere virtuali"

Non così ... molte classi non ne hanno bisogno. Ci sono così tanti esempi di dove non è necessario che sia sciocco elencarli, ma basta guardare attraverso la tua Libreria standard o dire boost e vedrai che c'è una grande maggioranza di classi che non hanno distruttori virtuali. Nel boost 1.53 conto 72 distruttori virtuali su 494.


23

In tal caso NON dovrei usare i distruttori virtuali?

  1. Per una classe concreta che non vuole essere ereditata.
  2. Per una classe base senza cancellazione polimorfica. Entrambi i client non dovrebbero essere in grado di eliminare polimorficamente usando un puntatore a Base.

BTW,

In quale caso usare i distruttori virtuali?

Per una classe base con cancellazione polimorfica.


7
+1 per # 2, in particolare senza eliminazione polimorfica . Se il tuo distruttore non può mai essere invocato tramite un puntatore di base, renderlo virtuale non è necessario e ridondante, soprattutto se la tua classe non era virtuale prima (quindi diventa di nuovo gonfiata con RTTI). Per proteggerti da qualsiasi utente che violi questo, come consigliato Herb Sutter, renderai il dtor della classe base protetto e non virtuale, in modo che possa essere invocato solo da / dopo un distruttore derivato.
underscore_d

@underscore_d Imho che un punto importante che mi mancava nelle risposte, poiché in presenza di eredità l'unico caso in cui non ho bisogno di un costruttore virtuale è quando posso assicurarmi che non sia mai necessario
precedentemente

14

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 Integerclasse 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 vtablein 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' vptrarchiviazione 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 vtablelato 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 objectclasse base centrale e tutte le funzioni in Java sono implicitamente virtuali (sostituibili ) in natura, salvo diversa indicazione. Di conseguenza, un Java Integertende anche a richiedere 16 byte di memoria su piattaforme a 64 bit come risultato di questi vptrmetadati di stile associati per istanza, ed è generalmente impossibile in Java avvolgere qualcosa come un singolo intin 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::vectorevita 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).


9

Perché c ++ non rende virtuali tutti i distruttori per impostazione predefinita? Costo della memoria aggiuntiva e chiamata della tabella dei metodi virtuali. Il C ++ viene utilizzato per la programmazione di sistemi, a bassa latenza, RT, laddove ciò potrebbe essere gravoso.


I distruttori non dovrebbero essere utilizzati in primo luogo nei sistemi in tempo reale poiché molte risorse come la memoria dinamica non possono essere utilizzate al fine di fornire forti scadenze
Marco A.

9
@MarcoA. Da quando i distruttori implicano l'allocazione dinamica della memoria?
chbaker0

@ chbaker0 Ho usato un 'mi piace'. Semplicemente non sono usati nella mia esperienza.
Marco A.,

6
È inoltre assurdo che la memoria dinamica non possa essere utilizzata nei sistemi hard in tempo reale. È abbastanza banale dimostrare che un heap preconfigurato con dimensioni di allocazione fisse e una bitmap di allocazione alloca memoria o restituisce una condizione di memoria esaurita nel tempo necessario per scansionare quella bitmap.
MSalters,

@msalters che mi fa pensare: immagina un programma in cui il costo di ogni operazione è stato memorizzato nel sistema dei tipi. Consentire controlli in fase di compilazione delle garanzie in tempo reale.
Yakk,

5

Questo è un buon esempio di quando non usare il distruttore virtuale: Da Scott Meyers:

Se una classe non contiene alcuna funzione virtuale, ciò indica spesso che non deve essere utilizzata come classe base. Quando una classe non è destinata a essere utilizzata come classe base, rendere virtuale il distruttore è generalmente una cattiva idea. Considera questo esempio, basato su una discussione in ARM:

// class for representing 2D points
class Point {
public:
    Point(short int xCoord, short int yCoord);
    ~Point();
private:
    short int x, y;
};

Se un int corto occupa 16 bit, un oggetto Point può inserirsi in un registro a 32 bit. Inoltre, un oggetto Point può essere passato come quantità a 32 bit a funzioni scritte in altre lingue come C o FORTRAN. Se il distruttore di Point viene reso virtuale, tuttavia, la situazione cambia.

Nel momento in cui aggiungi un membro virtuale, un puntatore virtuale viene aggiunto alla tua classe che punta alla tabella virtuale per quella classe.


If a class does not contain any virtual functions, that is often an indication that it is not meant to be used as a base class.Wut. Qualcun altro ricorda i Good Old Days, dove ci è stato permesso di usare le classi e l'eredità per costruire livelli successivi di membri e comportamenti riutilizzabili, senza doversi preoccupare affatto dei metodi virtuali? Dai, Scott. Ottengo il punto centrale, ma che "spesso" sta davvero raggiungendo.
underscore_d

3

Un distruttore virtuale aggiunge un costo di runtime. Il costo è particolarmente grande se la classe non ha altri metodi virtuali. Il distruttore virtuale è inoltre necessario solo in uno scenario specifico, in cui un oggetto viene eliminato o distrutto in altro modo tramite un puntatore a una classe base. In questo caso, il distruttore della classe base deve essere virtuale e il distruttore di qualsiasi classe derivata sarà implicitamente virtuale. Esistono alcuni scenari in cui una classe di base polimorfica viene utilizzata in modo tale che il distruttore non debba essere virtuale:

  • Se le istanze delle classi derivate non sono allocate nell'heap, ad esempio solo direttamente nello stack o all'interno di altri oggetti. (Tranne se si utilizza una memoria non inizializzata e un nuovo operatore di posizionamento.)
  • Se sull'heap sono allocate istanze di classi derivate, ma la cancellazione avviene solo tramite puntatori alla classe più derivata, ad esempio c'è un std::unique_ptr<Derived>, e il polimorfismo avviene solo tramite puntatori e riferimenti non proprietari. Un altro esempio è quando gli oggetti vengono allocati usando std::make_shared<Derived>(). Va bene usare std::shared_ptr<Base>fino a quando il puntatore iniziale era a std::shared_ptr<Derived>. Questo perché i puntatori condivisi hanno il loro dispacciamento dinamico per i distruttori (il deleter) che non si basa necessariamente su un distruttore di classe di base virtuale.

Naturalmente, qualsiasi convenzione di usare oggetti solo nei modi sopra menzionati può essere facilmente infranta. Pertanto, il consiglio di Herb Sutter rimane valido come sempre: "I distruttori della classe base devono essere pubblici e virtuali, oppure protetti e non virtuali". In questo modo, se qualcuno tenta di eliminare un puntatore a una classe base con distruttore non virtuale, molto probabilmente riceverà un errore di violazione dell'accesso al momento della compilazione.

Poi di nuovo ci sono classi che non sono progettate per essere classi (pubbliche) di base. Il mio consiglio personale è di renderli finalin C ++ 11 o versioni successive. Se è progettato per essere un piolo quadrato, è probabile che non funzionerà molto bene come un piolo rotondo. Ciò è correlato alla mia preferenza per avere un contratto di eredità esplicito tra la classe base e la classe derivata, per il modello di progettazione NVI (interfaccia non virtuale), per le classi base astratte piuttosto che concrete, e la mia avversione per le variabili membro protette, tra le altre cose , ma so che tutte queste opinioni sono in qualche modo controverse.


1

Dichiarare un distruttore virtualè necessario solo quando prevedi di renderlo classereditabile. Di solito le classi della libreria standard (come std::string) non forniscono un distruttore virtuale e quindi non sono pensate per la sottoclasse.


3
Il motivo è la sottoclasse + l'uso del polimorfismo. Un distruttore virtuale è richiesto solo se è necessaria una risoluzione dinamica, ovvero un riferimento / puntatore / qualunque cosa alla classe master possa effettivamente fare riferimento a un'istanza di una sottoclasse.
Michel Billaud,

2
@MichelBillaud in realtà puoi ancora avere polimorfismo senza dottori virtuali. Un dtor virtuale è richiesto SOLO per l'eliminazione polimorfica, vale a dire il richiamo di deleteun puntatore a una classe base.
chbaker0

1

Ci sarà un overhead nel costruttore per creare la vtable (se non hai altre funzioni virtuali, nel qual caso PROBABILMENTE, ma non sempre, dovresti avere anche un distruttore virtuale). E se non hai altre funzioni virtuali, il tuo oggetto ha una dimensione del puntatore più grande di quanto sia altrimenti necessario. Ovviamente, l'aumento delle dimensioni può avere un grande impatto su piccoli oggetti.

C'è una memoria aggiuntiva letta per ottenere la vtable e quindi chiamare la funzione indirectory attraverso quella, che è overhead sul distruttore non virtuale quando viene chiamato il distruttore. E ovviamente, di conseguenza, un piccolo codice aggiuntivo generato per ogni chiamata al distruttore. Questo è per i casi in cui il compilatore non può dedurre il tipo effettivo - in quei casi in cui può dedurre il tipo effettivo, il compilatore non utilizzerà la vtable, ma chiama direttamente il distruttore.

Si dovrebbe avere un distruttore virtuale se la classe è inteso come una classe base, in particolare se si può essere creato / distrutto da qualche altra entità rispetto al codice che sa che tipo è al momento della creazione, allora avete bisogno di un distruttore virtuale.

Se non si è sicuri, utilizzare il distruttore virtuale. È più facile rimuovere virtuale se si presenta come un problema piuttosto che cercare di trovare il bug causato da "non viene chiamato il distruttore giusto".

In breve, non dovresti avere un distruttore virtuale se: 1. Non hai nessuna funzione virtuale. 2. Non derivare dalla classe (contrassegnarlo finalin C ++ 11, in questo modo il compilatore dirà se si tenta di derivarne).

Nella maggior parte dei casi, la creazione e la distruzione non rappresentano una parte importante del tempo impiegato per utilizzare un oggetto particolare a meno che non ci sia "molto contenuto" (la creazione di una stringa da 1 MB richiederà ovviamente del tempo, poiché è necessario almeno 1 MB di dati essere copiato da dove si trova attualmente). Distruggere una stringa da 1 MB non è peggio della distruzione di una stringa da 150 B, entrambi richiedono deallocazione dell'archiviazione della stringa e non molto altro, quindi il tempo trascorso lì è in genere lo stesso [a meno che non sia una build di debug, in cui la deallocazione riempie spesso la memoria con un "modello di veleno" - ma non è così che eseguirai la tua vera applicazione in produzione].

In breve, c'è un piccolo overhead, ma per piccoli oggetti, può fare la differenza.

Si noti inoltre che i compilatori possono ottimizzare la ricerca virtuale in alcuni casi, quindi è solo una penalità

Come sempre quando si tratta di prestazioni, ingombro di memoria e simili: benchmark, profilo e misura, confronta i risultati con le alternative e osserva dove viene impiegata la maggior parte del tempo / memoria e non cercare di ottimizzare il 90% di codice che non viene eseguito molto [la maggior parte delle applicazioni ha circa il 10% di codice che influenza molto il tempo di esecuzione e il 90% di codice che non ha molta influenza]. Fallo con un alto livello di ottimizzazione, così avrai già il vantaggio che il compilatore faccia un buon lavoro! E ripeti, controlla di nuovo e migliora gradualmente. Non cercare di essere intelligente e cerca di capire cosa è importante e cosa non lo è a meno che tu non abbia molta esperienza con quel particolare tipo di applicazione.


1
"sarà un overhead nel costruttore per creare la vtable" - la vtable di solito "crea" su base per classe dal compilatore, con il costruttore che ha solo l'overhead di memorizzare un puntatore nell'istanza dell'oggetto in costruzione.
Tony,

Inoltre ... sto cercando di evitare l'ottimizzazione prematura, ma al contrario, You **should** have a virtual destructor if your class is intended as a base-classè una grossa semplificazione eccessiva - e pessimizzazione prematura . Ciò è necessario solo se a chiunque è consentito eliminare una classe derivata tramite il puntatore alla base. In molte situazioni, non è così. Se sai che lo è, allora sicuramente incorrere in spese generali. Che, a proposito, viene sempre aggiunto, anche se le chiamate effettive possono essere risolte staticamente dal compilatore. Altrimenti, quando controlli correttamente cosa le persone possono fare con i tuoi oggetti, non ne vale la pena
underscore_d
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.