Was ist ein vtable
?
Es kann hilfreich sein, zu wissen, worüber die Fehlermeldung spricht, bevor Sie versuchen, sie zu beheben. Ich werde auf einem hohen Niveau beginnen und dann auf einige weitere Details eingehen. Auf diese Weise können Benutzer überspringen, sobald sie mit dem Verständnis von vtables vertraut sind. … Und da springen gerade eine Menge Leute voran. :) Für diejenigen, die hier bleiben:
Eine vtable ist im Grunde die häufigste Implementierung von Polymorphismus in C ++ . Wenn vtables verwendet werden, hat jede polymorphe Klasse irgendwo im Programm eine vtable. Sie können es sich als (verstecktes) static
Datenelement der Klasse vorstellen. Jedes Objekt einer polymorphen Klasse ist der vtable für die am meisten abgeleitete Klasse zugeordnet. Durch Überprüfen dieser Zuordnung kann das Programm seine polymorphe Magie entfalten. Wichtige Einschränkung: Eine vtable ist ein Implementierungsdetail. Es wird vom C ++ - Standard nicht vorgeschrieben, obwohl die meisten (alle?) C ++ - Compiler vtables verwenden, um polymorphes Verhalten zu implementieren. Die Details, die ich präsentiere, sind entweder typische oder vernünftige Ansätze. Compiler dürfen davon abweichen!
Jedes polymorphe Objekt hat einen (versteckten) Zeiger auf die vtable für die am meisten abgeleitete Klasse des Objekts (in den komplexeren Fällen möglicherweise mehrere Zeiger). Durch Betrachten des Zeigers kann das Programm den "echten" Typ eines Objekts erkennen (außer während der Konstruktion, aber lassen Sie uns diesen Sonderfall überspringen). Wenn beispielsweise ein Objekt vom Typ A
nicht auf die vtable von verweist A
, ist dieses Objekt tatsächlich ein Unterobjekt von etwas, von dem abgeleitet wurde A
.
Der Name „VTable“ kommt von „ v irtual Funktion Tabelle “. In dieser Tabelle werden Zeiger auf (virtuelle) Funktionen gespeichert. Ein Compiler wählt seine Konvention für die Anordnung der Tabelle. Ein einfacher Ansatz besteht darin, die virtuellen Funktionen in der Reihenfolge zu durchlaufen, in der sie in den Klassendefinitionen deklariert sind. Wenn eine virtuelle Funktion aufgerufen wird, folgt das Programm dem Zeiger des Objekts auf eine vtable, geht zu dem Eintrag, der der gewünschten Funktion zugeordnet ist, und verwendet dann den gespeicherten Funktionszeiger, um die richtige Funktion aufzurufen. Es gibt verschiedene Tricks, um diese Arbeit zu machen, aber ich werde hier nicht darauf eingehen.
Wo / wann wird ein vtable
generiert?
Eine vtable wird vom Compiler automatisch generiert (manchmal als "emittiert" bezeichnet). Ein Compiler könnte in jeder Übersetzungseinheit, die eine polymorphe Klassendefinition sieht, eine vtable ausgeben, aber das wäre normalerweise ein unnötiger Overkill. Eine Alternative ( von gcc und wahrscheinlich von anderen verwendet) besteht darin, eine einzelne Übersetzungseinheit auszuwählen, in der die vtable platziert werden soll, ähnlich wie Sie eine einzelne Quelldatei auswählen würden, in die die statischen Datenelemente einer Klasse eingefügt werden sollen. Wenn bei diesem Auswahlprozess keine Übersetzungseinheiten ausgewählt werden, wird die vtable zu einer undefinierten Referenz. Daher der Fehler, dessen Botschaft zugegebenermaßen nicht besonders klar ist.
In ähnlicher Weise wird die vtable zu einer undefinierten Referenz, wenn der Auswahlprozess eine Übersetzungseinheit auswählt, diese Objektdatei jedoch nicht für den Linker bereitgestellt wird. Leider kann die Fehlermeldung in diesem Fall noch weniger deutlich sein als in dem Fall, in dem der Auswahlprozess fehlgeschlagen ist. (Vielen Dank an die Antwortenden, die diese Möglichkeit erwähnt haben. Ich hätte es sonst wahrscheinlich vergessen.)
Der von gcc verwendete Auswahlprozess ist sinnvoll, wenn wir mit der Tradition beginnen, jeder Klasse, die für ihre Implementierung eine benötigt, eine (einzelne) Quelldatei zuzuweisen. Es wäre schön, die vtable beim Kompilieren dieser Quelldatei auszugeben. Nennen wir das unser Ziel. Der Auswahlprozess muss jedoch funktionieren, auch wenn diese Tradition nicht befolgt wird. Anstatt nach der Implementierung der gesamten Klasse zu suchen, suchen wir nach der Implementierung eines bestimmten Mitglieds der Klasse. Wenn der Tradition gefolgt wird - und wenn dieses Mitglied tatsächlich umgesetzt wird - dann erreicht dies das Ziel.
Das von gcc (und möglicherweise von anderen Compilern) ausgewählte Mitglied ist die erste nicht inline virtuelle Funktion, die nicht rein virtuell ist. Wenn Sie Teil der Menge sind, die Konstruktoren und Destruktoren vor anderen Mitgliedsfunktionen deklariert, hat dieser Destruktor gute Chancen, ausgewählt zu werden. (Sie haben daran gedacht, den Destruktor virtuell zu machen, oder?) Es gibt Ausnahmen. Ich würde erwarten, dass die häufigsten Ausnahmen sind, wenn eine Inline-Definition für den Destruktor bereitgestellt wird und wenn der Standard-Destruktor angefordert wird (mit " = default
").
Der Kluge könnte bemerken, dass eine polymorphe Klasse Inline-Definitionen für alle ihre virtuellen Funktionen bereitstellen darf. Führt das nicht dazu, dass der Auswahlprozess fehlschlägt? Dies ist bei älteren Compilern der Fall. Ich habe gelesen, dass die neuesten Compiler diese Situation angegangen sind, kenne aber keine relevanten Versionsnummern. Ich könnte versuchen, dies nachzuschlagen, aber es ist einfacher, entweder Code zu verwenden oder darauf zu warten, dass sich der Compiler beschwert.
Zusammenfassend gibt es drei Hauptursachen für den Fehler "undefinierter Verweis auf vtable":
- Einer Mitgliedsfunktion fehlt ihre Definition.
- Eine Objektdatei wird nicht verknüpft.
- Alle virtuellen Funktionen haben Inline-Definitionen.
Diese Ursachen allein reichen nicht aus, um den Fehler selbst zu verursachen. Diese würden Sie vielmehr ansprechen, um den Fehler zu beheben. Erwarten Sie nicht, dass das absichtliche Erstellen einer dieser Situationen diesen Fehler definitiv hervorruft. Es gibt andere Anforderungen. Erwarten Sie, dass das Beheben dieser Situationen diesen Fehler behebt.
(OK, Nummer 3 könnte ausreichend gewesen sein, als diese Frage gestellt wurde.)
Wie behebe ich den Fehler?
Willkommen zurück, Leute, die vorausspringen! :) :)
- Sehen Sie sich Ihre Klassendefinition an. Suchen Sie die erste virtuelle Nicht-Inline-Funktion, die nicht rein virtuell ist (nicht "
= 0
") und deren Definition Sie angeben (nicht " = default
").
- Wenn es keine solche Funktion gibt, versuchen Sie, Ihre Klasse so zu ändern, dass es eine gibt. (Fehler möglicherweise behoben.)
- Siehe auch die Antwort von Philip Thomas für eine Einschränkung.
- Suchen Sie die Definition für diese Funktion. Wenn es fehlt, fügen Sie es hinzu! (Fehler möglicherweise behoben.)
- Überprüfen Sie Ihren Link-Befehl. Wenn die Objektdatei mit der Definition dieser Funktion nicht erwähnt wird, beheben Sie das! (Fehler möglicherweise behoben.)
- Wiederholen Sie die Schritte 2 und 3 für jede virtuelle Funktion und dann für jede nicht virtuelle Funktion, bis der Fehler behoben ist. Wenn Sie immer noch nicht weiterkommen, wiederholen Sie dies für jedes statische Datenelement.
Beispiel
Die Details der zu erledigenden Aufgaben können variieren und manchmal in separate Fragen verzweigen (z. B. Was ist ein undefinierter Verweis / ungelöster externer Symbolfehler und wie behebe ich ihn? ). Ich werde jedoch ein Beispiel dafür geben, was in einem bestimmten Fall zu tun ist, das neuere Programmierer verwirren könnte.
In Schritt 1 wird erwähnt, dass Ihre Klasse so geändert wird, dass sie eine Funktion eines bestimmten Typs hat. Wenn die Beschreibung dieser Funktion über Ihren Kopf ging, könnten Sie sich in der Situation befinden, die ich ansprechen möchte. Denken Sie daran, dass dies ein Weg ist, um das Ziel zu erreichen. Dies ist nicht der einzige Weg, und es könnte leicht bessere Wege in Ihrer spezifischen Situation geben. Rufen wir Ihre Klasse an A
. Ist Ihr Destruktor (in Ihrer Klassendefinition) als einer der beiden deklariert?
virtual ~A() = default;
oder
virtual ~A() {}
? In diesem Fall ändern zwei Schritte Ihren Destruktor in den gewünschten Funktionstyp. Ändern Sie zuerst diese Zeile in
virtual ~A();
Fügen Sie zweitens die folgende Zeile in eine Quelldatei ein, die Teil Ihres Projekts ist (vorzugsweise die Datei mit der Klassenimplementierung, falls vorhanden):
A::~A() {}
Dadurch ist Ihr (virtueller) Destruktor nicht inline und wird nicht vom Compiler generiert. (Sie können jederzeit Änderungen an Ihrem Code-Formatierungsstil vornehmen, z. B. einen Header-Kommentar zur Funktionsdefinition hinzufügen.)