Wir alle wissen, was virtuelle Funktionen in C ++ sind, aber wie werden sie auf einer tiefen Ebene implementiert?
Kann die vtable zur Laufzeit geändert oder sogar direkt aufgerufen werden?
Existiert die vtable für alle Klassen oder nur für diejenigen, die mindestens eine virtuelle Funktion haben?
Haben abstrakte Klassen einfach eine NULL für den Funktionszeiger mindestens eines Eintrags?
Verlangsamt eine einzige virtuelle Funktion die gesamte Klasse? Oder nur der Aufruf der virtuellen Funktion? Und wird die Geschwindigkeit beeinträchtigt, wenn die virtuelle Funktion tatsächlich überschrieben wird oder nicht, oder hat dies keine Auswirkung, solange sie virtuell ist?
c++
polymorphism
virtual-functions
vtable
Brian R. Bondy
quelle
quelle
Inside the C++ Object Model
von zu lesenStanley B. Lippman
. (Abschnitt 4.2, Seite 124-131)Antworten:
Wie werden virtuelle Funktionen auf einer tiefen Ebene implementiert?
Aus "Virtuelle Funktionen in C ++" :
Kann die vtable zur Laufzeit geändert oder sogar direkt aufgerufen werden?
Generell glaube ich, dass die Antwort "nein" ist. Sie könnten etwas Speicher-Mangeln durchführen, um die vtable zu finden, aber Sie würden immer noch nicht wissen, wie die Funktionssignatur aussieht, um sie aufzurufen. Alles, was Sie mit dieser Fähigkeit erreichen möchten (die von der Sprache unterstützt wird), sollte möglich sein, ohne direkt auf die vtable zuzugreifen oder sie zur Laufzeit zu ändern. Beachten Sie auch, dass in der C ++ - Sprachspezifikation nicht angegeben ist, dass vtables erforderlich sind. Auf diese Weise implementieren die meisten Compiler jedoch virtuelle Funktionen.
Existiert die vtable für alle Objekte oder nur für diejenigen, die mindestens eine virtuelle Funktion haben?
Ich glaube, die Antwort hier lautet "es hängt von der Implementierung ab", da die Spezifikation überhaupt keine vtables erfordert. In der Praxis glaube ich jedoch, dass alle modernen Compiler nur dann eine vtable erstellen, wenn eine Klasse mindestens eine virtuelle Funktion hat. Mit der vtable ist ein Speicherplatz-Overhead verbunden, und mit dem Aufrufen einer virtuellen Funktion gegenüber einer nicht-virtuellen Funktion ist ein Zeit-Overhead verbunden.
Haben abstrakte Klassen einfach eine NULL für den Funktionszeiger mindestens eines Eintrags?
Die Antwort ist, dass es nicht durch die Sprachspezifikation spezifiziert ist, also hängt es von der Implementierung ab. Das Aufrufen der reinen virtuellen Funktion führt zu undefiniertem Verhalten, wenn es nicht definiert ist (was normalerweise nicht der Fall ist) (ISO / IEC 14882: 2003 10.4-2). In der Praxis weist es der Funktion einen Steckplatz in der vtable zu, weist ihr jedoch keine Adresse zu. Dadurch bleibt die vtable unvollständig, sodass die abgeleiteten Klassen die Funktion implementieren und die vtable vervollständigen müssen. Einige Implementierungen setzen einfach einen NULL-Zeiger in den vtable-Eintrag. Andere Implementierungen platzieren einen Zeiger auf eine Dummy-Methode, die etwas Ähnliches wie eine Behauptung ausführt.
Beachten Sie, dass eine abstrakte Klasse eine Implementierung für eine reine virtuelle Funktion definieren kann, diese Funktion jedoch nur mit einer Syntax mit qualifizierter ID aufgerufen werden kann (dh die Klasse im Methodennamen vollständig angeben, ähnlich wie beim Aufrufen einer Basisklassenmethode von a abgeleitete Klasse). Dies geschieht, um eine benutzerfreundliche Standardimplementierung bereitzustellen, während eine abgeleitete Klasse weiterhin eine Überschreibung bereitstellen muss.
Verlangsamt eine einzelne virtuelle Funktion die gesamte Klasse oder nur den Aufruf der virtuellen Funktion?
Dies kommt an den Rand meines Wissens, also hilft mir bitte jemand hier raus, wenn ich falsch liege!
Ich glaube, dass nur die Funktionen, die in der Klasse virtuell sind, den Zeitleistungsverlust erfahren, der mit dem Aufrufen einer virtuellen Funktion im Vergleich zu einer nicht virtuellen Funktion verbunden ist. Der Platzbedarf für die Klasse ist in beiden Fällen vorhanden. Beachten Sie, dass bei einer vtable nur 1 pro Klasse und keine pro Objekt vorhanden ist .
Wird die Geschwindigkeit beeinträchtigt, wenn die virtuelle Funktion tatsächlich überschrieben wird oder nicht, oder hat dies keine Auswirkung, solange sie virtuell ist?
Ich glaube nicht, dass die Ausführungszeit einer überschriebenen virtuellen Funktion im Vergleich zum Aufruf der virtuellen Basisfunktion abnimmt. Es gibt jedoch einen zusätzlichen Speicherplatzaufwand für die Klasse, der mit dem Definieren einer anderen vtable für die abgeleitete Klasse gegenüber der Basisklasse verbunden ist.
Zusätzliche Ressourcen:
http://www.codersource.net/published/view/325/virtual_functions_in.aspx (über den Rückweg)
http://en.wikipedia.org/wiki/Virtual_table
http://www.codesourcery.com/public/ cxx-abi / abi.html # vtable
quelle
Nicht tragbar, aber wenn Ihnen schmutzige Tricks nichts ausmachen, sicher!
In den meisten Compilern, die ich gesehen habe, ist das vtbl * die ersten 4 Bytes des Objekts, und der vtbl-Inhalt ist einfach ein Array von Elementzeigern dort (im Allgemeinen in der Reihenfolge, in der sie deklariert wurden, mit dem ersten der Basisklasse). Es gibt natürlich auch andere mögliche Layouts, aber das habe ich allgemein beobachtet.
Nun, um ein paar Shenanigans zu ziehen ...
Ändern der Klasse zur Laufzeit:
Ersetzen einer Methode für alle Instanzen (Monkeypatching einer Klasse)
Dies ist etwas kniffliger, da sich das vtbl selbst wahrscheinlich im Nur-Lese-Speicher befindet.
Letzteres führt aufgrund der mprotect-Manipulationen eher dazu, dass Virenprüfer und der Link aufwachen und zur Kenntnis genommen werden. In einem Prozess, der das NX-Bit verwendet, kann dies durchaus fehlschlagen.
quelle
Verlangsamt eine einzige virtuelle Funktion die gesamte Klasse?
Virtuelle Funktionen verlangsamen die gesamte Klasse, sofern ein weiteres Datenelement initialisiert, kopiert usw. werden muss, wenn es sich um ein Objekt einer solchen Klasse handelt. Für eine Klasse mit etwa einem halben Dutzend Mitgliedern sollte der Unterschied vernachlässigbar sein. Für eine Klasse, die nur ein einzelnes
char
Mitglied oder überhaupt keine Mitglieder enthält , kann der Unterschied erheblich sein.Abgesehen davon ist zu beachten, dass nicht jeder Aufruf einer virtuellen Funktion ein virtueller Funktionsaufruf ist. Wenn Sie ein Objekt eines bekannten Typs haben, kann der Compiler Code für einen normalen Funktionsaufruf ausgeben und diese Funktion sogar inline einbinden, wenn er dies wünscht. Nur wenn Sie polymorphe Aufrufe über einen Zeiger oder eine Referenz ausführen, die möglicherweise auf ein Objekt der Basisklasse oder auf ein Objekt einer abgeleiteten Klasse verweisen, benötigen Sie die Indirektion vtable und zahlen für die Leistung.
Die Schritte, die die Hardware ausführen muss, sind im Wesentlichen dieselben, unabhängig davon, ob die Funktion überschrieben wird oder nicht. Die Adresse der vtable wird aus dem Objekt gelesen, der Funktionszeiger aus dem entsprechenden Slot abgerufen und die Funktion vom Zeiger aufgerufen. In Bezug auf die tatsächliche Leistung können Branchenvorhersagen einige Auswirkungen haben. Wenn sich die meisten Ihrer Objekte beispielsweise auf dieselbe Implementierung einer bestimmten virtuellen Funktion beziehen, besteht eine gewisse Wahrscheinlichkeit, dass der Verzweigungsprädiktor korrekt vorhersagt, welche Funktion aufgerufen werden soll, noch bevor der Zeiger abgerufen wurde. Es spielt jedoch keine Rolle, welche Funktion die häufigste ist: Es können die meisten Objekte sein, die an den nicht überschriebenen Basisfall delegieren, oder die meisten Objekte, die zu derselben Unterklasse gehören und daher an denselben überschriebenen Fall delegieren.
Wie werden sie auf einer tiefen Ebene umgesetzt?
Ich mag die Idee von Jheriko, dies anhand einer Scheinimplementierung zu demonstrieren. Aber ich würde C verwenden, um etwas zu implementieren, das dem obigen Code ähnelt, damit der niedrige Pegel leichter erkennbar ist.
Elternklasse Foo
abgeleitete Klasse Bar
Funktion f Ausführen eines virtuellen Funktionsaufrufs
Sie sehen also, eine vtable ist nur ein statischer Block im Speicher, der hauptsächlich Funktionszeiger enthält. Jedes Objekt einer polymorphen Klasse zeigt auf die vtable, die ihrem dynamischen Typ entspricht. Dies macht auch die Verbindung zwischen RTTI und virtuellen Funktionen klarer: Sie können überprüfen, welcher Typ eine Klasse ist, indem Sie einfach auf die vtable schauen, auf die sie zeigt. Das Obige wird in vielerlei Hinsicht vereinfacht, wie zum Beispiel Mehrfachvererbung, aber das allgemeine Konzept ist solide.
Wenn
arg
vom Typ istFoo*
und Sie nehmenarg->vtable
, aber tatsächlich ein Objekt vom Typ istBar
, dann erhalten Sie immer noch die richtige Adresse desvtable
. Das liegt daran, dass dasvtable
immer das erste Element an der Adresse des Objekts ist, egal ob es aufgerufen wirdvtable
oderbase.vtable
in einem korrekt eingegebenen Ausdruck.quelle
Normalerweise mit einer VTable ein Array von Zeigern auf Funktionen.
quelle
Diese Antwort wurde in die Antwort des Community-Wikis aufgenommen
Die Antwort darauf ist, dass es nicht spezifiziert ist - das Aufrufen der reinen virtuellen Funktion führt zu undefiniertem Verhalten, wenn es nicht definiert ist (was normalerweise nicht der Fall ist) (ISO / IEC 14882: 2003 10.4-2). Einige Implementierungen setzen einfach einen NULL-Zeiger in den vtable-Eintrag. Andere Implementierungen platzieren einen Zeiger auf eine Dummy-Methode, die etwas Ähnliches wie eine Behauptung ausführt.
Beachten Sie, dass eine abstrakte Klasse eine Implementierung für eine reine virtuelle Funktion definieren kann, diese Funktion jedoch nur mit einer Syntax mit qualifizierter ID aufgerufen werden kann (dh die Klasse im Methodennamen vollständig angeben, ähnlich wie beim Aufrufen einer Basisklassenmethode von a abgeleitete Klasse). Dies geschieht, um eine benutzerfreundliche Standardimplementierung bereitzustellen, während eine abgeleitete Klasse weiterhin eine Überschreibung bereitstellen muss.
quelle
Sie können die Funktionalität virtueller Funktionen in C ++ mithilfe von Funktionszeigern als Mitglieder einer Klasse und statischen Funktionen als Implementierungen oder mithilfe eines Zeigers auf Elementfunktionen und Elementfunktionen für die Implementierungen neu erstellen. Es gibt nur Vorteile für die Notation zwischen den beiden Methoden. Tatsächlich sind virtuelle Funktionsaufrufe nur eine Annehmlichkeit für die Notation. Tatsächlich ist die Vererbung nur eine Annehmlichkeit für die Notation. Sie kann alle implementiert werden, ohne die Sprachfunktionen für die Vererbung zu verwenden. :) :)
Das Folgende ist Mist ungetestet, wahrscheinlich fehlerhafter Code, aber hoffentlich demonstriert die Idee.
z.B
quelle
void(*)(Foo*) MyFunc;
Ist das eine Java-Syntax?Ich werde versuchen es einfach zu machen :)
Wir alle wissen, was virtuelle Funktionen in C ++ sind, aber wie werden sie auf einer tiefen Ebene implementiert?
Dies ist ein Array mit Zeigern auf Funktionen, die Implementierungen einer bestimmten virtuellen Funktion sind. Ein Index in diesem Array repräsentiert einen bestimmten Index einer für eine Klasse definierten virtuellen Funktion. Dies beinhaltet reine virtuelle Funktionen.
Wenn eine polymorphe Klasse von einer anderen polymorphen Klasse abgeleitet ist, können folgende Situationen auftreten:
Kann die vtable zur Laufzeit geändert oder sogar direkt aufgerufen werden?
Kein Standard - es gibt keine API, um darauf zuzugreifen. Compiler verfügen möglicherweise über einige Erweiterungen oder private APIs, um auf sie zuzugreifen. Dies ist jedoch möglicherweise nur eine Erweiterung.
Existiert die vtable für alle Klassen oder nur für diejenigen, die mindestens eine virtuelle Funktion haben?
Nur diejenigen, die mindestens eine virtuelle Funktion haben (sei es sogar Destruktor) oder mindestens eine Klasse ableiten, die ihre vtable hat ("ist polymorph").
Haben abstrakte Klassen einfach eine NULL für den Funktionszeiger mindestens eines Eintrags?
Das ist eine mögliche Implementierung, wird aber nicht praktiziert. Stattdessen gibt es normalerweise eine Funktion, die so etwas wie "reine virtuelle Funktion aufgerufen" druckt und ausführt
abort()
. Der Aufruf dazu kann auftreten, wenn Sie versuchen, die abstrakte Methode im Konstruktor oder Destruktor aufzurufen.Verlangsamt eine einzige virtuelle Funktion die gesamte Klasse? Oder nur der Aufruf der virtuellen Funktion? Und wird die Geschwindigkeit beeinträchtigt, wenn die virtuelle Funktion tatsächlich überschrieben wird oder nicht, oder hat dies keine Auswirkung, solange sie virtuell ist?
Die Verlangsamung hängt nur davon ab, ob der Anruf als direkter Anruf oder als virtueller Anruf aufgelöst wird. Und nichts anderes zählt. :) :)
Wenn Sie eine virtuelle Funktion über einen Zeiger oder einen Verweis auf ein Objekt aufrufen, wird sie immer als virtueller Aufruf implementiert, da der Compiler nie wissen kann, welche Art von Objekt diesem Zeiger zur Laufzeit zugewiesen wird und ob es sich um ein Objekt handelt Klasse, in der diese Methode überschrieben wird oder nicht. Nur in zwei Fällen kann der Compiler den Aufruf einer virtuellen Funktion als direkten Aufruf auflösen:
final
in der Klasse deklariert ist, auf die Sie einen Zeiger oder eine Referenz haben, über die Sie sie aufrufen ( nur in C ++ 11 ). In diesem Fall weiß der Compiler, dass diese Methode nicht weiter überschrieben werden kann und nur die Methode aus dieser Klasse sein kann.Beachten Sie jedoch, dass bei virtuellen Aufrufen nur zwei Zeiger dereferenziert werden müssen. Die Verwendung von RTTI (obwohl nur für polymorphe Klassen verfügbar) ist langsamer als das Aufrufen virtueller Methoden, falls Sie einen Fall finden, um dasselbe auf zwei Arten zu implementieren. Wenn Sie beispielsweise
virtual bool HasHoof() { return false; }
nur so definieren und dann überschreibenbool Horse::HasHoof() { return true; }
, dass Sie aufrufen könnenif (anim->HasHoof())
, ist dies schneller als der Versuchif(dynamic_cast<Horse*>(anim))
. Dies liegt darandynamic_cast
, dass die Klassenhierarchie in einigen Fällen sogar rekursiv durchlaufen werden muss, um festzustellen, ob der Pfad aus dem tatsächlichen Zeigertyp und dem gewünschten Klassentyp erstellt werden kann. Während der virtuelle Anruf immer der gleiche ist - zwei Zeiger dereferenzieren.quelle
Hier ist eine ausführbare manuelle Implementierung der virtuellen Tabelle in modernem C ++. Es hat eine gut definierte Semantik, keine Hacks und keine
void*
.Hinweis:
.*
und->*
sind andere Operatoren als*
und->
. Elementfunktionszeiger funktionieren anders.quelle
Jedes Objekt verfügt über einen vtable-Zeiger, der auf ein Array von Elementfunktionen verweist.
quelle
In all diesen Antworten wird hier nicht erwähnt, dass bei Mehrfachvererbung alle Basisklassen über virtuelle Methoden verfügen. Die erbende Klasse hat mehrere Zeiger auf eine vmt. Das Ergebnis ist, dass die Größe jeder Instanz eines solchen Objekts größer ist. Jeder weiß, dass eine Klasse mit virtuellen Methoden 4 Bytes zusätzlich für die vmt hat, aber im Falle einer Mehrfachvererbung ist es für jede Basisklasse, die virtuelle Methoden mal 4 hat. 4 ist die Größe des Zeigers.
quelle
Burlys Antworten sind hier bis auf die Frage richtig:
Haben abstrakte Klassen einfach eine NULL für den Funktionszeiger mindestens eines Eintrags?
Die Antwort ist, dass für abstrakte Klassen überhaupt keine virtuelle Tabelle erstellt wird. Es besteht keine Notwendigkeit, da keine Objekte dieser Klassen erstellt werden können!
Mit anderen Worten, wenn wir haben:
Der vtbl-Zeiger, auf den über pB zugegriffen wird, ist der vtbl der Klasse D. Genau so wird Polymorphismus implementiert. Das heißt, wie auf D-Methoden über pB zugegriffen wird. Für Klasse B ist kein vtbl erforderlich.
Als Antwort auf Mikes Kommentar unten ...
Wenn die B-Klasse in meiner Beschreibung eine virtuelle Methode foo () hat , die nicht von D überschrieben wird, und eine virtuelle Methodenleiste () , die überschrieben wird, hat Ds vtbl einen Zeiger auf Bs foo () und auf seine eigene Leiste () . Es ist noch kein vtbl für B erstellt.
quelle
B
sollte benötigt werden. Nur weil einige seiner Methoden (Standard-) Implementierungen haben, heißt das nicht, dass sie in einer vtable gespeichert werden müssen. Aber ich habe gerade Ihren Code durchlaufen (modulo einige Korrekturen, um ihn kompilieren zu lassen),gcc -S
gefolgt vonc++filt
und es gibt eindeutig eine vtable für darinB
enthalten. Ich denke, das könnte daran liegen, dass die vtable auch RTTI-Daten wie Klassennamen und Vererbung speichert. Es könnte für a erforderlich seindynamic_cast<B*>
. Auch-fno-rtti
lässt die vtable nicht verschwinden. Mitclang -O3
stattgcc
es ist plötzlich weg.sehr niedlicher Proof of Concept, den ich etwas früher gemacht habe (um zu sehen, ob die Reihenfolge der Vererbung wichtig ist); Lassen Sie mich wissen, ob Ihre Implementierung von C ++ es tatsächlich ablehnt (meine Version von gcc gibt nur eine Warnung zum Zuweisen anonymer Strukturen aus, aber das ist ein Fehler), ich bin neugierig.
CCPolite.h :
CCPolite_constructor.h :
main.c :
Ausgabe:
Beachten Sie, dass ich keine Zerstörung vornehmen muss, da ich mein gefälschtes Objekt niemals zuordne. Destruktoren werden automatisch am Ende des Bereichs dynamisch zugeordneter Objekte platziert, um den Speicher des Objektliteral selbst selbst und des vtable-Zeigers zurückzugewinnen.
quelle