Domanda

C'è qualche vero motivo non per rendere virtuale una funzione membro in C ++? Naturalmente, c'è sempre l'argomento delle prestazioni, ma questo non sembra rimanere nella maggior parte delle situazioni poiché il sovraccarico delle funzioni virtuali è abbastanza basso.

D'altra parte, sono stato morso un paio di volte dimenticandomi di rendere virtuale una funzione che dovrebbe essere virtuale. E quello sembra essere un argomento più grande di quello della performance. Quindi c'è qualche motivo per non rendere virtuali le funzioni membro per impostazione predefinita?

È stato utile?

Soluzione

Un modo per leggere le tue domande è " Perché C ++ non rende virtuale ogni funzione per impostazione predefinita, a meno che il programmatore ignori tale impostazione predefinita. " Senza consultare la mia copia di "Design ed Evolution di C ++": questo aggiungerebbe ulteriore spazio di archiviazione a ogni classe a meno che ogni funzione membro non sia resa non virtuale. Mi sembra che ciò avrebbe richiesto maggiori sforzi nell'implementazione del compilatore e avrebbe rallentato l'adozione del C ++ fornendo foraggio alle prestazioni ossessionate (mi conto in quel gruppo.)

Un altro modo per leggere le tue domande è " Perché i programmatori C ++ non rendono virtuale ogni funzione a meno che non abbiano ottime ragioni per non farlo? " La scusa della performance è probabilmente la ragione. A seconda dell'applicazione e del dominio, questa potrebbe essere una buona ragione o meno. Ad esempio, parte del mio team lavora in impianti di ticker di dati di mercato. Con oltre 100.000 messaggi / secondo su un singolo flusso, l'overhead della funzione virtuale sarebbe inaccettabile. Altre parti del mio team lavorano in complesse infrastrutture di trading. Rendere virtuale la maggior parte delle funzioni è probabilmente una buona idea in quel contesto, poiché la flessibilità aggiuntiva supera la micro-ottimizzazione.

Altri suggerimenti

Stroustrup, il progettista della lingua, dice :

  

Poiché molte classi non sono progettate per essere utilizzate come classi di base. Ad esempio, vedi complesso di classi .

     

Inoltre, gli oggetti di una classe con una funzione virtuale richiedono lo spazio necessario al meccanismo di chiamata della funzione virtuale, in genere una parola per oggetto. Questo sovraccarico può essere significativo e può ostacolare la compatibilità del layout con i dati di altre lingue (ad esempio C e Fortran).

     

Vedi The Design and Evolution of C ++ per ulteriori motivazioni di progettazione.

Esistono diversi motivi.

In primo luogo, prestazioni: Sì, l'overhead di una funzione virtuale è relativamente basso visto in isolamento. Ma impedisce anche al compilatore di essere in linea, e questa è una enorme fonte di ottimizzazione in C ++. La libreria standard C ++ funziona così come fa perché può incorporare dozzine e dozzine di piccole linee da cui è composta. Inoltre, una classe con metodi virtuali non è un tipo di dati POD e pertanto ad esso si applicano molte restrizioni. Non può essere copiato solo memcpy'ing, diventa più costoso da costruire e occupa più spazio. Ci sono molte cose che diventano improvvisamente illegali o meno efficienti quando viene coinvolto un tipo non POD.

E in secondo luogo, buona pratica OOP. Il punto in una classe è che fa una sorta di astrazione, nasconde i suoi dettagli interni e fornisce una garanzia che questa classe si comporterà così e così, e manterrà sempre questi invarianti. non non finirà mai in uno stato non valido " ;. È abbastanza difficile convivere se consenti ad altri di sovrascrivere qualsiasi funzione membro. Le funzioni membro definite nella classe sono lì per garantire che l'invariante sia mantenuto. Se non ci importasse, potremmo semplicemente rendere pubblici i membri interni dei dati e permettere alle persone di manipolarli a piacimento. Ma vogliamo che la nostra classe sia coerente. Ciò significa che dobbiamo specificare il comportamento della sua interfaccia pubblica. Ciò può comportare punti di personalizzazione specifici , rendendo virtuali le singole funzioni, ma quasi sempre implica anche la realizzazione di molti metodi non virtuali, in modo che possano fare il lavoro per garantire che l'invariante sia mantenuto. Il linguaggio dell'interfaccia non virtuale ne è un buon esempio: http://www.gotw.ca/publications/mill18.htm

Terzo, l'eredità non è spesso necessaria, specialmente non in C ++. I modelli e la programmazione generica (polimorfismo statico) possono in molti casi fare un lavoro migliore dell'ereditarietà (polimorfismo di runtime). Sì, a volte hai ancora bisogno di metodi virtuali ed ereditarietà, ma non è certamente l'impostazione predefinita. Se lo è, lo stai facendo in modo sbagliato. Lavora con la lingua, piuttosto che cercare di fingere che fosse qualcos'altro. Non è Java, e diversamente da Java, l'ereditarietà in C ++ è l'eccezione, non la regola.

Ignorerò le prestazioni e i costi di memoria, perché non ho modo di misurarli per il "quotato in generale" caso ...

Le classi con funzioni di membro virtuale non sono POD. Quindi, se vuoi usare la tua classe nel codice di basso livello che si basa sul fatto che è POD, allora (tra le altre restrizioni) qualsiasi funzione membro deve essere non virtuale.

Esempi di cose che puoi fare in modo portabile con un'istanza di una classe POD:

  • copiarlo con memcpy (a condizione che l'indirizzo di destinazione abbia un allineamento sufficiente).
  • accede ai campi con offsetof ()
  • in generale, trattalo come una sequenza di caratteri
  • ... um
  • questo è tutto. Sono sicuro di aver dimenticato qualcosa.

Altre cose che la gente ha detto di essere d'accordo:

  • Molte classi non sono progettate per l'ereditarietà. Rendere virtuali i loro metodi sarebbe fuorviante, poiché implica che le classi figlio potrebbero voler sovrascrivere il metodo e non dovrebbero esserci classi figlio.

  • Molti metodi non sono progettati per essere sovrascritti: stessa cosa.

Inoltre, anche quando le cose sono destinate a essere sottoclassate / sovrascritte, non sono necessariamente destinate al polimorfismo di runtime. Molto occasionalmente, nonostante ciò che dice la migliore pratica di OO, ciò che si desidera ereditare è il riutilizzo del codice. Ad esempio, se si utilizza CRTP per l'associazione dinamica simulata. Quindi, di nuovo, non vuoi implicare che la tua classe giocherà bene con il polimorfismo di runtime rendendo i suoi metodi virtuali, quando non dovrebbero mai essere chiamati in questo modo.

In sintesi, le cose che sono destinate a essere sovrascritte per il polimorfismo di runtime dovrebbero essere contrassegnate come virtuali e le cose che non lo sono, non dovrebbero. Se scopri che quasi tutte le tue funzioni membro sono destinate a essere virtuali, allora contrassegnale come virtuali a meno che non ci sia un motivo per non farlo. Se ritieni che la maggior parte delle funzioni dei membri non siano destinate a essere virtuali, non contrassegnarle come virtuali a meno che non vi sia un motivo per farlo.

È un problema complicato quando si progetta un'API pubblica, perché capovolgere un metodo dall'uno all'altro è un cambiamento decisivo, quindi è necessario ottenerlo giusto la prima volta. Ma non devi necessariamente sapere prima di avere degli utenti, se i tuoi utenti vorranno "quotare polimorfo" le tue lezioni. Ho hum. L'approccio del contenitore STL, che definisce interfacce astratte e vieta del tutto l'ereditarietà, è sicuro ma a volte richiede agli utenti di scrivere di più.

Il seguente post è principalmente opinione, ma qui va:

Il design orientato agli oggetti è tre cose e l'incapsulamento (nascondere le informazioni) è la prima di queste cose. Se un design di classe non è solido su questo, allora il resto non ha molta importanza.

È stato detto prima che "l'eredità rompe l'incapsulamento" (Alan Snyder '86) Una buona discussione di questo è presente nel gruppo di quattro libri di design. Una classe dovrebbe essere progettata per supportare l'ereditarietà in un modo molto specifico. Altrimenti, si apre la possibilità di uso improprio da parte degli eredi.

Vorrei fare l'analogia che rendere virtuali tutti i tuoi metodi è simile a rendere pubblici tutti i tuoi membri. Un po 'allungato, lo so, ma è per questo che ho usato la parola "analogia"

Il linguaggio dell'interfaccia non virtuale utilizza metodi non virtuali. Per ulteriori informazioni, consultare Herb Sutter " Virtuality " articolo.

http://www.gotw.ca/publications/mill18.htm

E commenti sul linguaggio NVI:

http://www.parashift.com /c++-faq-lite/strange-inheritance.html#faq-23.3 http://accu.org/index.php/journals/269 [Vedi sotto -sezione]

Mentre stai progettando la tua gerarchia di classi, può avere senso scrivere una funzione che non deve essere ignorata. Un esempio è se stai facendo il metodo "quot template" " modello, in cui si dispone di un metodo pubblico che chiama diversi metodi virtuali privati. Non vorresti che le classi derivate lo sovrascrivessero; tutti dovrebbero usare la definizione di base.

Non è presente " final " parola chiave, quindi il modo migliore per comunicare ad altri sviluppatori che un metodo non deve essere ignorato è renderlo non virtuale. (oltre ai commenti facilmente ignorabili)

A livello di classe, rendere il distruttore non virtuale comunica che la classe non deve essere utilizzata come classe di base, ad esempio i contenitori STL.

Rendere un metodo non virtuale comunica come dovrebbe essere usato.

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