Eredità
L'intero punto dell'ereditarietà è condividere un'interfaccia e un protocollo comuni tra molte diverse implementazioni in modo tale che un'istanza di una classe derivata possa essere trattata in modo identico a qualsiasi altra istanza da qualsiasi altro tipo derivato.
In C ++ l'eredità porta anche con sé i dettagli dell'implementazione, contrassegnare (o non contrassegnare) il distruttore come virtuale è uno di questi dettagli di implementazione.
Funzione Binding
Ora quando viene chiamata una funzione, o uno dei suoi casi speciali come un costruttore o un distruttore, il compilatore deve scegliere quale implementazione di funzione intendesse. Quindi deve generare il codice macchina che segue questa intenzione.
Il modo più semplice per farlo sarebbe selezionare la funzione in fase di compilazione ed emettere il codice macchina in modo tale che, indipendentemente da qualsiasi valore, quando quel pezzo di codice viene eseguito, esegue sempre il codice per la funzione. Funziona benissimo tranne che per l'eredità.
Se abbiamo una classe base con una funzione (potrebbe essere una qualsiasi funzione, incluso il costruttore o il distruttore) e il tuo codice chiama una funzione su di essa, cosa significa?
Prendendo dal tuo esempio, se hai chiamato initialize_vector()
il compilatore devi decidere se intendevi davvero chiamare l'implementazione trovata in Base
, o l'implementazione trovata in Derived
. Esistono due modi per decidere questo:
- Il primo è decidere che, poiché hai chiamato da un
Base
tipo, intendevi l'implementazione in Base
.
- Il secondo è decidere che, poiché
Base
potrebbe essere il tipo di runtime del valore memorizzato nel valore digitato Base
, o Derived
che la decisione su quale chiamata effettuare, deve essere presa in fase di esecuzione quando viene chiamata (ogni volta che viene chiamata).
Il compilatore a questo punto è confuso, entrambe le opzioni sono ugualmente valide. Questo è quando virtual
entra nel mix. Quando questa parola chiave è presente, il compilatore seleziona l'opzione 2 ritardando la decisione tra tutte le possibili implementazioni fino a quando il codice viene eseguito con un valore reale. Quando questa parola chiave è assente, il compilatore seleziona l'opzione 1 perché è il comportamento altrimenti normale.
Il compilatore potrebbe comunque selezionare l'opzione 1 nel caso di una chiamata di funzione virtuale. Ma solo se può dimostrare che è sempre così.
Costruttori e distruttori
Quindi perché non specifichiamo un costruttore virtuale?
Più intuitivamente come sceglierebbe il compilatore tra implementazioni identiche del costruttore per Derived
e Derived2
? Questo è piuttosto semplice, non può. Non esiste un valore preesistente da cui il compilatore può apprendere ciò che era realmente previsto. Non esiste alcun valore preesistente perché quello è il lavoro del costruttore.
Quindi perché dobbiamo specificare un distruttore virtuale?
Più intuitivamente come sceglierebbe il compilatore tra le implementazioni per Base
e Derived
? Sono solo chiamate di funzione, quindi si verifica il comportamento della chiamata di funzione. Senza un distruttore virtuale dichiarato, il compilatore deciderà di collegarsi direttamente al Base
distruttore indipendentemente dal tipo di runtime dei valori.
In molti compilatori, se il derivato non dichiara alcun membro dei dati, né eredita da altri tipi, il comportamento nel ~Base()
sarà adatto, ma non è garantito. Funzionava puramente per caso, proprio come stare di fronte a un lanciafiamme che non era ancora stato acceso. Stai bene per un po '.
L'unico modo corretto per dichiarare qualsiasi tipo di base o interfaccia in C ++ è dichiarare un distruttore virtuale, in modo che venga chiamato il distruttore corretto per ogni istanza data della gerarchia di tipi di quel tipo. Ciò consente alla funzione con la maggior conoscenza dell'istanza di ripulirla correttamente.
~derived()
delegato al distruttore di VEC. In alternativa, stai assumendo cheunique_ptr<base> pt
conoscerebbe il distruttore derivato. Senza un metodo virtuale, questo non può essere il caso. Mentre a unique_ptr può essere assegnata una funzione di eliminazione che è un parametro modello senza alcuna rappresentazione di runtime e tale funzione non è di alcuna utilità per questo codice.