Che cos'è un vtable
?
Potrebbe essere utile sapere di cosa parla il messaggio di errore prima di provare a risolverlo. Inizierò ad un livello elevato, quindi passerò a qualche dettaglio in più. In questo modo le persone possono saltare una volta che si sentono a proprio agio con la loro comprensione dei Vtables. ... e ci sono un sacco di persone che saltano avanti proprio ora. :) Per quelli che restano in giro:
Una vtable è sostanzialmente l'implementazione più comune del polimorfismo in C ++ . Quando vengono usate vtables, ogni classe polimorfica ha una vtable da qualche parte nel programma; puoi pensarlo come un static
membro di dati (nascosto) della classe. Ogni oggetto di una classe polimorfica è associato alla vtable per la sua classe più derivata. Controllando questa associazione, il programma può operare la sua magia polimorfica. Avvertenza importante: una vtable è un dettaglio di implementazione. Non è richiesto dallo standard C ++, anche se la maggior parte dei compilatori C ++ (tutti?) Usano vtables per implementare il comportamento polimorfico. I dettagli che presento sono approcci tipici o ragionevoli. I compilatori possono deviare da questo!
Ogni oggetto polimorfico ha un puntatore (nascosto) sulla vtable per la classe più derivata dell'oggetto (possibilmente più puntatori, nei casi più complessi). Guardando il puntatore, il programma può dire qual è il tipo "reale" di un oggetto (tranne durante la costruzione, ma saltiamo quel caso speciale). Ad esempio, se un oggetto di tipo A
non punta alla vtable di A
, allora quell'oggetto è in realtà un oggetto secondario di qualcosa derivato A
.
Il nome "vtable" deriva da " v funzione irtual tavolo ". È una tabella che memorizza i puntatori a funzioni (virtuali). Un compilatore sceglie la sua convenzione per come è disposta la tabella; un approccio semplice è quello di passare attraverso le funzioni virtuali nell'ordine in cui sono dichiarate all'interno delle definizioni di classe. Quando viene chiamata una funzione virtuale, il programma segue il puntatore dell'oggetto su una vtable, passa alla voce associata alla funzione desiderata, quindi utilizza il puntatore della funzione memorizzata per invocare la funzione corretta. Ci sono vari trucchi per farlo funzionare, ma non entrerò in quelli qui.
Dove / quando viene vtable
generato?
Una vtable viene generata automaticamente (a volte chiamata "emessa") dal compilatore. Un compilatore potrebbe emettere una vtable in ogni unità di traduzione che vede una definizione di classe polimorfica, ma che di solito sarebbe inutile eccessivo. Un'alternativa ( usata da gcc , e probabilmente da altri) è quella di scegliere una singola unità di traduzione in cui posizionare la vtable, in modo simile a come si selezionerebbe un singolo file sorgente in cui inserire i membri di dati statici di una classe. Se questo processo di selezione non riesce a scegliere alcuna unità di traduzione, la vtable diventa un riferimento indefinito. Da qui l'errore, il cui messaggio è certamente non particolarmente chiaro.
Allo stesso modo, se il processo di selezione sceglie un'unità di traduzione, ma quel file oggetto non viene fornito al linker, allora la vtable diventa un riferimento indefinito. Sfortunatamente, il messaggio di errore può essere ancora meno chiaro in questo caso rispetto al caso in cui il processo di selezione non è riuscito. (Grazie ai rispondenti che hanno menzionato questa possibilità. Probabilmente l'avrei dimenticata altrimenti.)
Il processo di selezione usato da gcc ha senso se iniziamo con la tradizione di dedicare un (singolo) file sorgente a ciascuna classe che ne ha bisogno per la sua implementazione. Sarebbe bello emettere la vtable durante la compilazione del file sorgente. Chiamiamo questo il nostro obiettivo. Tuttavia, il processo di selezione deve funzionare anche se questa tradizione non viene seguita. Quindi, invece di cercare l'implementazione dell'intera classe, cerchiamo l'implementazione di un membro specifico della classe. Se viene seguita la tradizione - e se quel membro è effettivamente implementato - allora questo raggiunge l'obiettivo.
Il membro selezionato da gcc (e potenzialmente da altri compilatori) è la prima funzione virtuale non in linea che non è pura virtuale. Se fai parte della folla che dichiara costruttori e distruttori prima che funzioni altri membri, allora quel distruttore ha buone probabilità di essere selezionato. (Ti sei ricordato di rendere virtuale il distruttore, vero?) Ci sono eccezioni; Mi aspetto che le eccezioni più comuni siano quando viene fornita una definizione inline per il distruttore e quando viene richiesto il distruttore predefinito (usando " = default
").
L'astuto potrebbe notare che una classe polimorfica è autorizzata a fornire definizioni in linea per tutte le sue funzioni virtuali. Ciò non causa il fallimento del processo di selezione? Lo fa nei compilatori più vecchi. Ho letto che gli ultimi compilatori hanno affrontato questa situazione, ma non conosco i numeri di versione pertinenti. Potrei provare a cercarlo, ma è più facile scrivere il codice o attendere che il compilatore si lamenti.
In breve, ci sono tre cause principali dell'errore "riferimento indefinito a vtable":
- A una funzione membro manca la sua definizione.
- Un file oggetto non viene collegato.
- Tutte le funzioni virtuali hanno definizioni incorporate.
Queste cause sono di per sé insufficienti a causare l'errore da sole. Piuttosto, questi sono ciò che vorresti affrontare per risolvere l'errore. Non aspettarti che la creazione intenzionale di una di queste situazioni produrrà sicuramente questo errore; ci sono altri requisiti. Aspettatevi che la risoluzione di queste situazioni risolva questo errore.
(OK, il numero 3 potrebbe essere stato sufficiente quando è stata posta questa domanda.)
Come correggere l'errore?
Bentornati gente che salta avanti! :)
- Guarda la definizione della tua classe. Trova la prima funzione virtuale non in linea che non è pura virtuale (non "
= 0
") e di cui fornisci la definizione (non " = default
").
- Se non esiste tale funzione, prova a modificare la tua classe in modo che ce ne sia una. (Errore eventualmente risolto.)
- Vedi anche la risposta di Philip Thomas per un avvertimento.
- Trova la definizione per quella funzione. Se manca, aggiungilo! (Errore eventualmente risolto.)
- Controlla il tuo comando di collegamento. Se non menziona il file oggetto con la definizione di quella funzione, correggilo! (Errore eventualmente risolto.)
- Ripetere i passaggi 2 e 3 per ciascuna funzione virtuale, quindi per ciascuna funzione non virtuale, fino alla risoluzione dell'errore. Se sei ancora bloccato, ripeti l'operazione per ciascun membro di dati statici.
Esempio
I dettagli di cosa fare possono variare e talvolta si ramificano in domande separate (come Cosa è un riferimento indefinito / errore di simbolo esterno non risolto e come posso risolverlo? ). Fornirò tuttavia un esempio di cosa fare in un caso specifico che potrebbe confondere i programmatori più recenti.
Il passaggio 1 menziona la modifica della classe in modo che abbia una funzione di un certo tipo. Se la descrizione di quella funzione ti passasse per la testa, potresti trovarti nella situazione che intendo affrontare. Tieni presente che questo è un modo per raggiungere l'obiettivo; non è l'unico modo e ci potrebbero essere facilmente modi migliori nella tua situazione specifica. Chiamiamo la tua classe A
. Il distruttore è dichiarato (nella definizione di classe) come uno dei due
virtual ~A() = default;
o
virtual ~A() {}
? In tal caso, due passaggi trasformeranno il distruttore nel tipo di funzione che desideriamo. Innanzitutto, cambia quella linea in
virtual ~A();
In secondo luogo, inserisci la seguente riga in un file sorgente che fa parte del tuo progetto (preferibilmente il file con l'implementazione della classe, se ne hai uno):
A::~A() {}
Ciò rende il tuo distruttore (virtuale) non in linea e non generato dal compilatore. (Sentiti libero di modificare le cose per adattarle meglio al tuo stile di formattazione del codice, come aggiungere un commento di intestazione alla definizione della funzione.)