Perché devo dichiarare le funzioni virtuali come tali?

2

Esempio: Abbiamo una classe di base Base e tre sottoclassi che implementano tutte la propria versione di doSomething() .

In una funzione intermedia f(Base b) , vogliamo chiamare la versione pertinente di doSomething() a seconda dell'oggetto che è stato passato.

Perché dobbiamo dichiarare doSomething come funzione virtuale in Base ? Sicuramente b punta ora sull'oggetto corretto in modo che quando chiamiamo b.doSomething , sappiamo quale metodo deve essere chiamato.

Domanda laterale: se b punta semplicemente a un oggetto, perché ha bisogno di un tipo?

Stranamente, questa volta aveva perfettamente senso, ma sto iniziando ad avere dubbi e le domande stanno sorgendo ...

    
posta M-R 21.05.2016 - 22:58
fonte

4 risposte

12

Poiché le funzioni virtuali hanno un costo delle prestazioni di runtime e parte della filosofia di C ++ è "non si paga per ciò che non si usa".

In particolare, i metodi virtuali devono dereferenziare un puntatore per scoprire dove si trova il metodo corretto per l'oggetto specifico su cui sono stati chiamati. Un puntatore dereferenziato per chiamata potrebbe non sembrare molto, ma questo impedisce anche che la funzione non sia mai in linea, e devi fare riferimento all'oggetto stesso tramite puntatore o riferimento per una chiamata di funzione virtuale al lavoro in primo luogo (altrimenti tu ottenere "oggetti che affettano" che possono avere ripercussioni più ampie).

Solitamente, questo non è davvero un costo molto grande, motivo per cui alcuni linguaggi come Java possono farcela con cose come "fare tutti i riferimenti alle variabili di classe" e "rendere tutti i metodi virtuali di default", presumibilmente nella speranza di creare un linguaggio più semplice per le applicazioni che possono essere sviluppate senza una comprensione dettagliata di come viene tipicamente implementata ciascuna caratteristica linguistica.

Side Question: If "b" is simply pointing to an object, why does it even need a type?

Se vuoi veramente , puoi usare void* , che è effettivamente un puntatore senza tipo. Ma normalmente non dovresti farlo perché i puntatori correttamente digitati aggiungono un minimo di sicurezza di tipo. E la maggior parte delle volte, dovresti usare puntatori intelligenti come std::unique_ptr che sono ancora più sicuri dato che automaticamente deallocano la memoria a cui puntano quando hai finito con loro.

    
risposta data 21.05.2016 - 23:27
fonte
5

Java

Riguardo a Java, la tua premessa è completamente errata: non devi mai specificare che una funzione è virtuale - tutte le funzioni sono virtuali di default. Java fornisce final per specificare che un metodo non può essere sovrascritto, il che ha un effetto un po 'simile, ma non è esattamente identico. Nella misura in cui final significa "non virtuale", è ancora fatto nella direzione opposta: fondamentalmente e "opt-out" piuttosto che "opt-in".

C ++

Una delle cose che Bjarne ha sempre sottolineato su come progettare oggetti è stabilire e mantenere invarianti. Alcuni di questi invarianti hanno a che fare con valori, come "questo valore deve essere compreso tra 1 e 100" o "questo valore deve essere compreso tra 2 e 3 volte quel valore", ecc.

È ugualmente importante, tuttavia, stabilire e mantenere invarianti rispetto al comportamento di un oggetto. Questi invarianti stabiliscono una struttura all'interno della quale parti specifiche possono variare. A parte una programmazione davvero brutta, consente al programmatore di specificare e applicare determinati comportamenti tramite il meccanismo di controllo del tipo del compilatore.

Ad esempio, consideriamo un vettore C ++. Lo standard C ++ garantisce che un vettore C ++ fornisce una crescita costante ammortizzata. Se permettessimo di sovrascrivere la funzione di crescita di un vettore, una classe derivata da vector potrebbe violare quel vincolo, ma dal momento che è (ipoteticamente) derivata pubblicamente da vector , può essere utilizzata in qualsiasi posizione necessaria per un vettore, incluso il codice ciò può dipendere dal fatto che il vincolo viene applicato.

Rendere le funzioni virtuali è anche orientato all'utilizzo dell'ereditarietà. Bjarne ha (sempre così gentilmente) sottolineato per anni che molti programmatori sfruttano l'ereditarietà. È perfettamente adatto per progettare classi che non richiedono o utilizzare funzioni virtuali in tutti 1 .

Quindi no, questa decisione in C ++ non è interamente (o anche principalmente) per evitare l'overhead di chiamare una funzione virtuale. Principalmente si tratta di design di base e oggetti che rafforzano gli invarianti. Molti di questi invarianti si trovano sui dati di un oggetto, ma alcuni sono anche sul comportamento di un oggetto. Data l'importanza dell'impostazione degli invarianti, le funzioni dovrebbero essere non virtuali per impostazione predefinita, quindi gli invarianti vengono applicati per impostazione predefinita e il comportamento è aperto alle modifiche solo dove previsto.

1. Ad esempio, considera la sua intervista 2003 con Bill Venners .

    
risposta data 26.05.2016 - 01:28
fonte
0

Surely "b" now points to the correct object so that when we call b.doSomething, we know which method needs to be called.

Sicuramente no!

Il problema chiave è se b punta a o fa riferimento a una classe base ma in realtà è una classe derivata, è il metodo doSomething definito in quella classe derivata che deve essere chiamata, anziché il doSomething metodo la classe base.

Alcune lingue trasportano sempre quel bagaglio e rendono ogni funzione l'equivalente di una funzione virtuale C ++. C ++ ti dà la possibilità di non dover portare quel bagaglio in giro. Il vantaggio di non portare quel bagaglio in giro per metodi non virtuali è che la decisione diventa una decisione sul tempo di compilazione. Il compilatore e il linker sanno esattamente quale funzione dovrebbe essere chiamata; il costo di runtime è zero.

Il sovraccarico nel decidere quale funzione chiamare durante il runtime non ha importanza se la funzione in questione viene chiamata ma poche volte all'ora. Quel sovraccarico di runtime si accumula in maniera massiccia se la funzione viene invece chiamata decine di migliaia di volte al secondo (o anche più).

    
risposta data 22.05.2016 - 01:43
fonte
0

Oltre alle prestazioni ecc. ...

Supponi che la tua classe base abbia un metodo doSomething. Supponi che la tua classe derivata non si preoccupi di fare Qualcosa qualunque. Lo sviluppatore ha dimenticato che esiste persino. Lo sviluppatore della classe derivata vuole anche creare un metodo che faccia qualcosa e lo chiama DoSomething, dosomething, do_something o qualcosa del genere.

Se il nuovo metodo, che non ha nulla a che fare con Qualcosa nella classe base, è anche chiamato doSomething, allora avrai un problema. Se la lingua può fare qualsiasi cosa per evitare quel problema, sarebbe grandioso.

O supponiamo al contrario, lo sviluppatore vuole ignorare doSomething, ma usa erroneamente l'ortografia sbagliata del nome. Di nuovo, se la lingua può fare qualsiasi cosa per evitare quel problema, sarebbe fantastico.

    
risposta data 26.05.2016 - 18:22
fonte

Leggi altre domande sui tag