Wie werden virtuelle Funktionen und vtable implementiert?

109

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?

Brian R. Bondy
quelle
2
Schlagen Sie vor, das Meisterwerk Inside the C++ Object Modelvon zu lesen Stanley B. Lippman. (Abschnitt 4.2, Seite 124-131)
smwikipedia

Antworten:

123

Wie werden virtuelle Funktionen auf einer tiefen Ebene implementiert?

Aus "Virtuelle Funktionen in C ++" :

Immer wenn in einem Programm eine virtuelle Funktion deklariert ist, wird eine av - Tabelle für die Klasse erstellt. Die V-Tabelle besteht aus Adressen der virtuellen Funktionen für Klassen, die eine oder mehrere virtuelle Funktionen enthalten. Das Objekt der Klasse, die die virtuelle Funktion enthält, enthält einen virtuellen Zeiger, der auf die Basisadresse der virtuellen Tabelle im Speicher zeigt. Bei jedem virtuellen Funktionsaufruf wird die V-Tabelle verwendet, um die Funktionsadresse aufzulösen. Ein Objekt der Klasse, das eine oder mehrere virtuelle Funktionen enthält, enthält einen virtuellen Zeiger namens vptr ganz am Anfang des Objekts im Speicher. Daher nimmt die Größe des Objekts in diesem Fall um die Größe des Zeigers zu. Dieser vptr enthält die Basisadresse der virtuellen Tabelle im Speicher. Beachten Sie, dass virtuelle Tabellen klassenspezifisch sind, dh Es gibt nur eine virtuelle Tabelle für eine Klasse, unabhängig von der Anzahl der darin enthaltenen virtuellen Funktionen. Diese virtuelle Tabelle enthält wiederum die Basisadressen einer oder mehrerer virtueller Funktionen der Klasse. Zu dem Zeitpunkt, an dem eine virtuelle Funktion für ein Objekt aufgerufen wird, stellt der vptr dieses Objekts die Basisadresse der virtuellen Tabelle für diese Klasse im Speicher bereit. Diese Tabelle wird zum Auflösen des Funktionsaufrufs verwendet, da sie die Adressen aller virtuellen Funktionen dieser Klasse enthält. So wird die dynamische Bindung während eines virtuellen Funktionsaufrufs aufgelöst. Der vptr dieses Objekts stellt die Basisadresse der virtuellen Tabelle für diese Klasse im Speicher bereit. Diese Tabelle wird zum Auflösen des Funktionsaufrufs verwendet, da sie die Adressen aller virtuellen Funktionen dieser Klasse enthält. So wird die dynamische Bindung während eines virtuellen Funktionsaufrufs aufgelöst. Der vptr dieses Objekts stellt die Basisadresse der virtuellen Tabelle für diese Klasse im Speicher bereit. Diese Tabelle wird zum Auflösen des Funktionsaufrufs verwendet, da sie die Adressen aller virtuellen Funktionen dieser Klasse enthält. So wird die dynamische Bindung während eines virtuellen Funktionsaufrufs aufgelöst.

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

Zach Burlingame
quelle
2
Es würde nicht im Einklang mit Stroustrups C ++ - Philosophie stehen, wenn ein Compiler einen unnötigen vtable-Zeiger in ein Objekt einfügt, das ihn nicht benötigt. Die Regel ist, dass Sie keinen Overhead bekommen, der nicht in C ist, es sei denn, Sie fragen danach, und es ist unhöflich für Compiler, das zu brechen.
Steve Jessop
3
Ich bin damit einverstanden, dass es für jeden Compiler, der sich selbst ernst nimmt, dumm wäre, eine vtable zu verwenden, wenn keine virtuellen Funktionen vorhanden sind. Ich hielt es jedoch für wichtig, darauf hinzuweisen, dass der C ++ - Standard meines Wissens nicht / erforderlich / ist. Seien Sie also gewarnt, bevor Sie davon abhängen.
Zach Burlingame
8
Auch virtuelle Funktionen können nicht virtuell aufgerufen werden. Dies ist in der Tat recht häufig: Wenn sich das Objekt auf dem Stapel befindet, kennt der Compiler innerhalb des Bereichs den genauen Typ und optimiert die vtable-Suche. Dies gilt insbesondere für den dtor, der im selben Stack-Bereich aufgerufen werden muss.
MSalters
1
Ich glaube, wenn eine Klasse mindestens eine virtuelle Funktion hat, hat jedes Objekt eine vtable und nicht eine für die gesamte Klasse.
Asaf R
3
Allgemeine Implementierung: Jedes Objekt hat einen Zeiger auf eine vtable. Die Klasse besitzt den Tisch. Die Konstruktionsmagie besteht einfach darin, den vtable-Zeiger im abgeleiteten ctor zu aktualisieren, nachdem der Basis-ctor fertig ist.
MSalters
31
  • Kann die vtable zur Laufzeit geändert oder sogar direkt aufgerufen werden?

Nicht tragbar, aber wenn Ihnen schmutzige Tricks nichts ausmachen, sicher!

WARNUNG : Diese Technik wird nicht für Kinder, Erwachsene unter 969 Jahren oder kleine pelzige Kreaturen von Alpha Centauri empfohlen . Zu den Nebenwirkungen können Dämonen gehören, die aus Ihrer Nase fliegen , das plötzliche Auftreten von Yog-Sothoth als erforderlicher Genehmiger bei allen nachfolgenden Codeüberprüfungen oder die rückwirkende Hinzufügung IHuman::PlayPiano()zu allen vorhandenen Instanzen.]

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.

class A {
  public:
  virtual int f1() = 0;
};
class B : public A {
  public:
  virtual int f1() { return 1; }
  virtual int f2() { return 2; }
};
class C : public A {
  public:
  virtual int f1() { return -1; }
  virtual int f2() { return -2; }
};

A *x = new B;
A *y = new C;
A *z = new C;

Nun, um ein paar Shenanigans zu ziehen ...

Ändern der Klasse zur Laufzeit:

std::swap(*(void **)x, *(void **)y);
// Now x is a C, and y is a B! Hope they used the same layout of members!

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.

int f3(A*) { return 0; }

mprotect(*(void **)x,8,PROT_READ|PROT_WRITE|PROT_EXEC);
// Or VirtualProtect on win32; this part's very OS-specific
(*(int (***)(A *)x)[0] = f3;
// Now C::f1() returns 0 (remember we made x into a C above)
// so x->f1() and z->f1() both return 0

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.

Puetzk
quelle
6
Hmm. Es fühlt sich bedrohlich an, dass dies ein Kopfgeld erhalten hat. Ich hoffe, das heißt nicht, dass @Mobilewits solche Shenanigans für eine gute Idee hält ...
Puetzk
1
Bitte denken Sie daran, die Verwendung dieser Technik klar und deutlich zu unterbinden, anstatt zu "zwinkern".
Einpoklum
" vtbl-Inhalte sind einfach ein Array von Mitgliedszeigern " eigentlich ist es ein Datensatz (eine Struktur) mit verschiedenen Einträgen, die zufällig gleichmäßig verteilt sind
neugieriger Kerl
1
Sie können es so oder so betrachten; Die Funktionszeiger haben unterschiedliche Signaturen und damit unterschiedliche Zeigertypen. In diesem Sinne ist es tatsächlich strukturell. In anderen Kontexten ist die Idee des vtbl-Index jedoch nützlich (z. B. verwendet ActiveX ihn so, wie er zwei Schnittstellen in Typelibs beschreibt). Dies ist eine eher Array-ähnliche Ansicht.
Puetzk
17

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?

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 charMitglied 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.

struct Foo { virtual ~Foo(); virtual int a() { return 1; } };
struct Bar: public Foo { int a() { return 2; } };
void f(Foo& arg) {
  Foo x; x.a(); // non-virtual: always calls Foo::a()
  Bar y; y.a(); // non-virtual: always calls Bar::a()
  arg.a();      // virtual: must dispatch via vtable
  Foo z = arg;  // copy constructor Foo::Foo(const Foo&) will convert to Foo
  z.a();        // non-virtual Foo::a, since z is a Foo, even if arg was not
}

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

typedef struct Foo_t Foo;   // forward declaration
struct slotsFoo {           // list all virtual functions of Foo
  const void *parentVtable; // (single) inheritance
  void (*destructor)(Foo*); // virtual destructor Foo::~Foo
  int (*a)(Foo*);           // virtual function Foo::a
};
struct Foo_t {                      // class Foo
  const struct slotsFoo* vtable;    // each instance points to vtable
};
void destructFoo(Foo* self) { }     // Foo::~Foo
int aFoo(Foo* self) { return 1; }   // Foo::a()
const struct slotsFoo vtableFoo = { // only one constant table
  0,                                // no parent class
  destructFoo,
  aFoo
};
void constructFoo(Foo* self) {      // Foo::Foo()
  self->vtable = &vtableFoo;        // object points to class vtable
}
void copyConstructFoo(Foo* self,
                      Foo* other) { // Foo::Foo(const Foo&)
  self->vtable = &vtableFoo;        // don't copy from other!
}

abgeleitete Klasse Bar

typedef struct Bar_t {              // class Bar
  Foo base;                         // inherit all members of Foo
} Bar;
void destructBar(Bar* self) { }     // Bar::~Bar
int aBar(Bar* self) { return 2; }   // Bar::a()
const struct slotsFoo vtableBar = { // one more constant table
  &vtableFoo,                       // can dynamic_cast to Foo
  (void(*)(Foo*)) destructBar,      // must cast type to avoid errors
  (int(*)(Foo*)) aBar
};
void constructBar(Bar* self) {      // Bar::Bar()
  self->base.vtable = &vtableBar;   // point to Bar vtable
}

Funktion f Ausführen eines virtuellen Funktionsaufrufs

void f(Foo* arg) {                  // same functionality as above
  Foo x; constructFoo(&x); aFoo(&x);
  Bar y; constructBar(&y); aBar(&y);
  arg->vtable->a(arg);              // virtual function call
  Foo z; copyConstructFoo(&z, arg);
  aFoo(&z);
  destructFoo(&z);
  destructBar(&y);
  destructFoo(&x);
}

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 argvom Typ ist Foo*und Sie nehmen arg->vtable, aber tatsächlich ein Objekt vom Typ ist Bar, dann erhalten Sie immer noch die richtige Adresse des vtable. Das liegt daran, dass das vtableimmer das erste Element an der Adresse des Objekts ist, egal ob es aufgerufen wird vtableoder base.vtablein einem korrekt eingegebenen Ausdruck.

MvG
quelle
"Jedes Objekt einer polymorphen Klasse zeigt auf seine eigene Tabelle." Wollen Sie damit sagen, dass jedes Objekt seine eigene Tabelle hat? AFAIK vtable wird von allen Objekten derselben Klasse gemeinsam genutzt. Lass es mich wissen, wenn ich falsch liege.
Bhuwan
1
@Bhuwan: Nein, Sie haben Recht: Es gibt nur eine vtable pro Typ (bei Vorlagen möglicherweise pro Vorlageninstanziierung). Ich wollte damit sagen, dass jedes Objekt einer polymorphen Klasse mit einem Verweis auf die für sie geltende vtable, sodass jedes Objekt einen solchen Zeiger hat, aber für Objekte desselben Typs auf dieselbe Tabelle verweist. Wahrscheinlich sollte ich das umformulieren.
MvG
1
@MvG " Objekte des gleichen Typs, die auf dieselbe Tabelle verweisen ", nicht während der Erstellung von Basisklassen mit virtuellen Basisklassen! (ein ganz besonderer Fall)
Neugieriger
1
@curiousguy: Ich würde das unter "Das oben Gesagte wird in vielerlei Hinsicht vereinfacht" ablegen, zumal die Hauptanwendung von virtuellen Basen die Mehrfachvererbung ist, die ich auch nicht modelliert habe. Aber danke für den Kommentar, es ist nützlich, dies hier für Leute zu haben, die möglicherweise mehr Tiefe benötigen.
MvG
3

Normalerweise mit einer VTable ein Array von Zeigern auf Funktionen.

Lou Franco
quelle
2

Diese Antwort wurde in die Antwort des Community-Wikis aufgenommen

  • Haben abstrakte Klassen einfach eine NULL für den Funktionszeiger mindestens eines Eintrags?

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.

Michael Burr
quelle
Ich glaube auch nicht, dass eine abstrakte Klasse eine Implementierung für eine reine virtuelle Funktion definieren kann. Per Definition hat eine reine virtuelle Funktion keinen Körper (z. B. bool my_func () = 0;). Sie können jedoch Implementierungen für reguläre virtuelle Funktionen bereitstellen.
Zach Burlingame
Eine reine virtuelle Funktion kann eine Definition haben. Siehe Scott Meyers '"Effective C ++, 3rd Ed", Artikel 34, ISO 14882-2003 10.4-2, oder bytes.com/forum/thread572745.html
Michael Burr,
2

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

class Foo
{
protected:
 void(*)(Foo*) MyFunc;
public:
 Foo() { MyFunc = 0; }
 void ReplciatedVirtualFunctionCall()
 {
  MyFunc(*this);
 }
...
};

class Bar : public Foo
{
private:
 static void impl1(Foo* f)
 {
  ...
 }
public:
 Bar() { MyFunc = impl1; }
...
};

class Baz : public Foo
{
private:
 static void impl2(Foo* f)
 {
  ...
 }
public:
 Baz() { MyFunc = impl2; }
...
};
Jheriko
quelle
void(*)(Foo*) MyFunc;Ist das eine Java-Syntax?
Neugieriger
Nein, die C / C ++ - Syntax für Funktionszeiger. Um mich selbst zu zitieren: "Sie können die Funktionalität virtueller Funktionen in C ++ mithilfe von Funktionszeigern neu erstellen." Es ist eine unangenehme Syntax, aber etwas, mit dem Sie vertraut sein sollten, wenn Sie sich als C-Programmierer betrachten.
Jheriko
Ein AC-Funktionszeiger würde eher so aussehen: int ( PROC) (); und ein Zeiger auf eine Klassenmitgliedsfunktion würde folgendermaßen aussehen: int (ClassName :: MPROC) ();
Bedrohung
1
@menace, du hast dort eine Syntax vergessen ... denkst du vielleicht an das typedef? typedef int (* PROC) (); Sie können also PROC foo später anstelle von int (* foo) () ausführen?
Jheriko
2

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:

  • Die abgeleitete Klasse fügt keine neuen virtuellen Funktionen hinzu und überschreibt keine. In diesem Fall teilt diese Klasse die vtable mit der Basisklasse.
  • Die abgeleitete Klasse fügt virtuelle Methoden hinzu und überschreibt sie. In diesem Fall erhält es eine eigene vtable, in der die hinzugefügten virtuellen Funktionen einen Index haben, der nach dem zuletzt abgeleiteten Index beginnt.
  • Mehrere polymorphe Klassen in der Vererbung. In diesem Fall haben wir eine Indexverschiebung zwischen der zweiten und der nächsten Basis und deren Index in der abgeleiteten Klasse

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:

  • Wenn Sie die Methode über einen Wert aufrufen (eine Variable oder das Ergebnis einer Funktion, die einen Wert zurückgibt), hat der Compiler in diesem Fall keine Zweifel an der tatsächlichen Klasse des Objekts und kann sie beim Kompilieren "hart auflösen" .
  • Wenn die virtuelle Methode finalin 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 überschreiben bool Horse::HasHoof() { return true; }, dass Sie aufrufen können if (anim->HasHoof()), ist dies schneller als der Versuch if(dynamic_cast<Horse*>(anim)). Dies liegt daran dynamic_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.

Ethouris
quelle
2

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.

#include <iostream>
#include <vector>
#include <memory>

struct vtable; // forward declare, we need just name

class animal
{
public:
    const std::string& get_name() const { return name; }

    // these will be abstract
    bool has_tail() const;
    bool has_wings() const;
    void sound() const;

protected: // we do not want animals to be created directly
    animal(const vtable* vtable_ptr, std::string name)
    : vtable_ptr(vtable_ptr), name(std::move(name)) { }

private:
    friend vtable; // just in case for non-public methods

    const vtable* const vtable_ptr;
    std::string name;
};

class cat : public animal
{
public:
    cat(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does meow\n"; 
    }
};

class dog : public animal
{
public:
    dog(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does whoof\n"; 
    }
};

class parrot : public animal
{
public:
    parrot(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return false; }
    bool has_wings() const { return true; }
    void sound() const
    {
        std::cout << get_name() << " does crrra\n"; 
    }
};

// now the magic - pointers to member functions!
struct vtable
{
    bool (animal::* const has_tail)() const;
    bool (animal::* const has_wings)() const;
    void (animal::* const sound)() const;

    // constructor
    vtable (
        bool (animal::* const has_tail)() const,
        bool (animal::* const has_wings)() const,
        void (animal::* const sound)() const
    ) : has_tail(has_tail), has_wings(has_wings), sound(sound) { }
};

// global vtable objects
const vtable vtable_cat(
    static_cast<bool (animal::*)() const>(&cat::has_tail),
    static_cast<bool (animal::*)() const>(&cat::has_wings),
    static_cast<void (animal::*)() const>(&cat::sound));
const vtable vtable_dog(
    static_cast<bool (animal::*)() const>(&dog::has_tail),
    static_cast<bool (animal::*)() const>(&dog::has_wings),
    static_cast<void (animal::*)() const>(&dog::sound));
const vtable vtable_parrot(
    static_cast<bool (animal::*)() const>(&parrot::has_tail),
    static_cast<bool (animal::*)() const>(&parrot::has_wings),
    static_cast<void (animal::*)() const>(&parrot::sound));

// set vtable pointers in constructors
cat::cat(std::string name) : animal(&vtable_cat, std::move(name)) { }
dog::dog(std::string name) : animal(&vtable_dog, std::move(name)) { }
parrot::parrot(std::string name) : animal(&vtable_parrot, std::move(name)) { }

// implement dynamic dispatch
bool animal::has_tail() const
{
    return (this->*(vtable_ptr->has_tail))();
}

bool animal::has_wings() const
{
    return (this->*(vtable_ptr->has_wings))();
}

void animal::sound() const
{
    (this->*(vtable_ptr->sound))();
}

int main()
{
    std::vector<std::unique_ptr<animal>> animals;
    animals.push_back(std::make_unique<cat>("grumpy"));
    animals.push_back(std::make_unique<cat>("nyan"));
    animals.push_back(std::make_unique<dog>("doge"));
    animals.push_back(std::make_unique<parrot>("party"));

    for (const auto& a : animals)
        a->sound();

    // note: destructors are not dispatched virtually
}
Xeverous
quelle
1

Jedes Objekt verfügt über einen vtable-Zeiger, der auf ein Array von Elementfunktionen verweist.


quelle
1

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.

Philip Stuyck
quelle
0

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:

class B { ~B() = 0; }; // Abstract Base class
class D : public B { ~D() {} }; // Concrete Derived class

D* pD = new D();
B* pB = pD;

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.

Andrew Stein
quelle
Dies ist aus zwei Gründen nicht korrekt: 1) Eine abstrakte Klasse kann zusätzlich zu reinen virtuellen Methoden reguläre virtuelle Methoden haben, und 2) reine virtuelle Methoden können optional eine Definition haben, die mit einem vollständig qualifizierten Namen aufgerufen werden kann.
Michael Burr
Richtig - beim zweiten Gedanken stelle ich mir vor, dass der Compiler, wenn alle virtuellen Methoden rein virtuell wären, die vtable weg optimieren könnte (er würde Hilfe vom Linker benötigen, um sicherzustellen, dass es auch keine Definitionen gibt).
Michael Burr
1
" Die Antwort ist, dass für abstrakte Klassen überhaupt keine virtuelle Tabelle erstellt wird. " Falsch. " Es besteht keine Notwendigkeit, da keine Objekte dieser Klassen erstellt werden können! " Falsch.
Neugieriger
Ich kann Ihre Begründung , dass kein VTable für folgen 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 -Sgefolgt von c++filtund es gibt eindeutig eine vtable für darin Benthalten. 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 sein dynamic_cast<B*>. Auch -fno-rttilässt die vtable nicht verschwinden. Mit clang -O3statt gcces ist plötzlich weg.
MvG
@MvG " Nur weil einige seiner Methoden (Standard-) Implementierungen haben, heißt das nicht, dass sie in einer vtable gespeichert werden müssen. " Ja, das bedeutet genau das.
Neugieriger
0

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 :

#ifndef CCPOLITE_H
#define CCPOLITE_H

/* the vtable or interface */
typedef struct {
    void (*Greet)(void *);
    void (*Thank)(void *);
} ICCPolite;

/**
 * the actual "object" literal as C++ sees it; public variables be here too 
 * all CPolite objects use(are instances of) this struct's structure.
 */
typedef struct {
    ICCPolite *vtbl;
} CPolite;

#endif /* CCPOLITE_H */

CCPolite_constructor.h :

/** 
 * unconventionally include me after defining OBJECT_NAME to automate
 * static(allocation-less) construction.
 *
 * note: I assume CPOLITE_H is included; since if I use anonymous structs
 *     for each object, they become incompatible and cause compile time errors
 *     when trying to do stuff like assign, or pass functions.
 *     this is similar to how you can't pass void * to windows functions that
 *         take handles; these handles use anonymous structs to make 
 *         HWND/HANDLE/HINSTANCE/void*/etc not automatically convertible, and
 *         require a cast.
 */
#ifndef OBJECT_NAME
    #error CCPolite> constructor requires object name.
#endif

CPolite OBJECT_NAME = {
    &CCPolite_Vtbl
};

/* ensure no global scope pollution */
#undef OBJECT_NAME

main.c :

#include <stdio.h>
#include "CCPolite.h"

// | A Greeter is capable of greeting; nothing else.
struct IGreeter
{
    virtual void Greet() = 0;
};

// | A Thanker is capable of thanking; nothing else.
struct IThanker
{
    virtual void Thank() = 0;
};

// | A Polite is something that implements both IGreeter and IThanker
// | Note that order of implementation DOES MATTER.
struct IPolite1 : public IGreeter, public IThanker{};
struct IPolite2 : public IThanker, public IGreeter{};

// | implementation if IPolite1; implements IGreeter BEFORE IThanker
struct CPolite1 : public IPolite1
{
    void Greet()
    {
        puts("hello!");
    }

    void Thank()
    {
        puts("thank you!");
    }
};

// | implementation if IPolite1; implements IThanker BEFORE IGreeter
struct CPolite2 : public IPolite2
{
    void Greet()
    {
        puts("hi!");
    }

    void Thank()
    {
        puts("ty!");
    }
};

// | imposter Polite's Greet implementation.
static void CCPolite_Greet(void *)
{
    puts("HI I AM C!!!!");
}

// | imposter Polite's Thank implementation.
static void CCPolite_Thank(void *)
{
    puts("THANK YOU, I AM C!!");
}

// | vtable of the imposter Polite.
ICCPolite CCPolite_Vtbl = {
    CCPolite_Thank,
    CCPolite_Greet    
};

CPolite CCPoliteObj = {
    &CCPolite_Vtbl
};

int main(int argc, char **argv)
{
    puts("\npart 1");
    CPolite1 o1;
    o1.Greet();
    o1.Thank();

    puts("\npart 2");    
    CPolite2 o2;    
    o2.Greet();
    o2.Thank();    

    puts("\npart 3");    
    CPolite1 *not1 = (CPolite1 *)&o2;
    CPolite2 *not2 = (CPolite2 *)&o1;
    not1->Greet();
    not1->Thank();
    not2->Greet();
    not2->Thank();

    puts("\npart 4");        
    CPolite1 *fake = (CPolite1 *)&CCPoliteObj;
    fake->Thank();
    fake->Greet();

    puts("\npart 5");        
    CPolite2 *fake2 = (CPolite2 *)fake;
    fake2->Thank();
    fake2->Greet();

    puts("\npart 6");        
    #define OBJECT_NAME fake3
    #include "CCPolite_constructor.h"
    fake = (CPolite1 *)&fake3;
    fake->Thank();
    fake->Greet();

    puts("\npart 7");        
    #define OBJECT_NAME fake4
    #include "CCPolite_constructor.h"
    fake2 = (CPolite2 *)&fake4;
    fake2->Thank();
    fake2->Greet();    

    return 0;
}

Ausgabe:

part 1
hello!
thank you!

part 2
hi!
ty!

part 3
ty!
hi!
thank you!
hello!

part 4
HI I AM C!!!!
THANK YOU, I AM C!!

part 5
THANK YOU, I AM C!!
HI I AM C!!!!

part 6
HI I AM C!!!!
THANK YOU, I AM C!!

part 7
THANK YOU, I AM C!!
HI I AM C!!!!

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.

Dmitry
quelle