C ++ vtables. Teil 2 (Virtuelle Vererbung + Compiler-generierter Code)

Die Übersetzung des Artikels wurde speziell für Studierende des Kurses "C ++ Developer" erstellt . Ist es interessant, sich in diese Richtung zu entwickeln? Sehen Sie sich die Aufzeichnung der Google Test Framework Practice Class an!



Teil 3 - Virtuelle Vererbung


Im ersten und zweiten Teil dieses Artikels haben wir darüber gesprochen, wie vtables in den einfachsten Fällen und dann in mehrfacher Vererbung funktionieren. Virtuelle Vererbung macht die Situation noch komplizierter.


Wie Sie sich vielleicht erinnern, bedeutet virtuelle Vererbung, dass es in einer bestimmten Klasse nur eine Instanz der Basisklasse gibt. Zum Beispiel:


class ios ... class istream : virtual public ios ... class ostream : virtual public ios ... class iostream : public istream, public ostream 

iostream oben genannte virtual Schlüsselwort iostream tatsächlich zwei Instanzen von ios , die während der Synchronisierung Kopfschmerzen verursachen könnten und einfach ineffektiv wären.


Um die virtuelle Vererbung zu verstehen, betrachten wir das folgende Codefragment:


 #include <iostream> using namespace std; class Grandparent { public: virtual void grandparent_foo() {} int grandparent_data; }; class Parent1 : virtual public Grandparent { public: virtual void parent1_foo() {} int parent1_data; }; class Parent2 : virtual public Grandparent { public: virtual void parent2_foo() {} int parent2_data; }; class Child : public Parent1, public Parent2 { public: virtual void child_foo() {} int child_data; }; int main() { Child child; } 

Lass uns das child erforschen. Ich beginne damit, eine große Menge an Speicher genau dort vtable Child , wo das vtable Child startet, wie wir es in den vorherigen Abschnitten getan haben, und dann die Ergebnisse zu analysieren. Ich schlage vor, hier einen kurzen Blick auf das Ergebnis zu werfen und darauf zurückzukommen, wenn ich die folgenden Details offenlege.


 (gdb) p child $1 = {<Parent1> = {<Grandparent> = {_vptr$Grandparent = 0x400998 <vtable for Child+96>, grandparent_data = 0}, _vptr$Parent1 = 0x400950 <vtable for Child+24>, parent1_data = 0}, <Parent2> = {_vptr$Parent2 = 0x400978 <vtable for Child+64>, parent2_data = 4195888}, child_data = 0} (gdb) x/600xb 0x400938 0x400938 <vtable for Child>: 0x20 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400940 <vtable for Child+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400948 <vtable for Child+16>: 0x00 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400950 <vtable for Child+24>: 0x70 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400958 <vtable for Child+32>: 0xa0 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400960 <vtable for Child+40>: 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400968 <vtable for Child+48>: 0xf0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400970 <vtable for Child+56>: 0x00 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400978 <vtable for Child+64>: 0x90 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400980 <vtable for Child+72>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400988 <vtable for Child+80>: 0xe0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400990 <vtable for Child+88>: 0x00 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400998 <vtable for Child+96>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x4009a0 <VTT for Child>: 0x50 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009a8 <VTT for Child+8>: 0xf8 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009b0 <VTT for Child+16>: 0x18 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009b8 <VTT for Child+24>: 0x98 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009c0 <VTT for Child+32>: 0xb8 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009c8 <VTT for Child+40>: 0x98 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009d0 <VTT for Child+48>: 0x78 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009d8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4009e0 <construction vtable for Parent1-in-Child>: 0x20 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4009e8 <construction vtable for Parent1-in-Child+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4009f0 <construction vtable for Parent1-in-Child+16>: 0x50 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009f8 <construction vtable for Parent1-in-Child+24>: 0x70 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400a00 <construction vtable for Parent1-in-Child+32>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a08 <construction vtable for Parent1-in-Child+40>: 0xe0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400a10 <construction vtable for Parent1-in-Child+48>: 0x50 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a18 <construction vtable for Parent1-in-Child+56>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400a20 <typeinfo name for Parent1>: 0x37 0x50 0x61 0x72 0x65 0x6e 0x74 0x31 0x400a28 <typeinfo name for Parent1+8>: 0x00 0x31 0x31 0x47 0x72 0x61 0x6e 0x64 0x400a30 <typeinfo name for Grandparent+7>: 0x70 0x61 0x72 0x65 0x6e 0x74 0x00 0x00 0x400a38 <typeinfo for Grandparent>: 0x50 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400a40 <typeinfo for Grandparent+8>: 0x29 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a48: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a50 <typeinfo for Parent1>: 0xa0 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400a58 <typeinfo for Parent1+8>: 0x20 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a60 <typeinfo for Parent1+16>: 0x00 0x00 0x00 0x00 0x01 0x00 0x00 0x00 0x400a68 <typeinfo for Parent1+24>: 0x38 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a70 <typeinfo for Parent1+32>: 0x03 0xe8 0xff 0xff 0xff 0xff 0xff 0xff 0x400a78: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a80 <construction vtable for Parent2-in-Child>: 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a88 <construction vtable for Parent2-in-Child+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a90 <construction vtable for Parent2-in-Child+16>: 0xd0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a98 <construction vtable for Parent2-in-Child+24>: 0x90 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400aa0 <construction vtable for Parent2-in-Child+32>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400aa8 <construction vtable for Parent2-in-Child+40>: 0xf0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400ab0 <construction vtable for Parent2-in-Child+48>: 0xd0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400ab8 <construction vtable for Parent2-in-Child+56>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400ac0 <typeinfo name for Parent2>: 0x37 0x50 0x61 0x72 0x65 0x6e 0x74 0x32 0x400ac8 <typeinfo name for Parent2+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400ad0 <typeinfo for Parent2>: 0xa0 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400ad8 <typeinfo for Parent2+8>: 0xc0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400ae0 <typeinfo for Parent2+16>: 0x00 0x00 0x00 0x00 0x01 0x00 0x00 0x00 0x400ae8 <typeinfo for Parent2+24>: 0x38 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400af0 <typeinfo for Parent2+32>: 0x03 0xe8 0xff 0xff 0xff 0xff 0xff 0xff 0x400af8 <typeinfo name for Child>: 0x35 0x43 0x68 0x69 0x6c 0x64 0x00 0x00 0x400b00 <typeinfo for Child>: 0xa0 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400b08 <typeinfo for Child+8>: 0xf8 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b10 <typeinfo for Child+16>: 0x02 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x400b18 <typeinfo for Child+24>: 0x50 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b20 <typeinfo for Child+32>: 0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400b28 <typeinfo for Child+40>: 0xd0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b30 <typeinfo for Child+48>: 0x02 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x400b38 <vtable for Grandparent>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400b40 <vtable for Grandparent+8>: 0x38 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b48 <vtable for Grandparent+16>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 

Wow, es gibt viele Informationen. Zwei neue Fragen vtable for X-in-Child sofort auf: Was ist VTT und was ist die Konstruktionstabelle vtable for X-in-Child ? Wir werden sie schnellstmöglich beantworten.
Beginnen wir mit der Kinderspeicherstruktur:


GrößeWert
8 Bytes_vptr $ Parent1
4 Bytesparent1_data (+ 4 Auffüllbytes)
8 Bytes_vptr $ Parent2
4 Bytesparent2_data
4 Byteschild_data
8 Bytes_vptr $ Großeltern
4 Bytesgrandparent_data (+ 4 Füllbytes)

In der Tat hat Child nur 1 Instanz von Großeltern. Das Nichttriviale ist, dass er der Letzte im Gedächtnis ist, obwohl er der Höchste in der Hierarchie ist.
Hier ist die vtable Struktur:


Die AdresseWertInhalt
0x4009380x20 (32)Virtual-Base-Offset (wir werden das bald besprechen)
0x4009400top_offset
0x4009480x400b00typeinfo für kind
0x4009500x400870Parent1 :: parent1_foo (). Der vtable-Zeiger Parent1 zeigt hier.
0x4009580x4008a0Child :: child_foo ()
0x4009600x10 (16)Virtual-Base-Offset
0x400968-16top_offset
0x40090x400btypeinfo für kind
7000
0x4009780x400890Parent2 :: parent2_foo (). Der vtable-Zeiger Parent2 zeigt hier.
0x4009800Virtual-Base-Offset
0x400988-32top_offset
0x4009900x400b00typeinfo für kind
0x4009980x400880Grandparent :: grandparent_foo (). Der vtable-Zeiger Grandparent zeigt hier.

Oben gibt es ein neues Konzept - virtual-base offset . Bald werden wir verstehen, was er dort tut.
Lassen Sie uns als Nächstes diese seltsam aussehenden construction vtables . Hier ist die Konstruktionstabelle vtable for Parent1-in-Child :


WertInhalt
0x20 (32)Virtual-Base-Offset
0oben versetzt
0x400a50TypInfo für Parent1
0x400870Parent1 :: parent1_foo ()
0Virtual-Base-Offset
-32oben versetzt
0x400a50TypInfo für Parent1
0x400880Großeltern :: grandparent_foo ()

Im Moment halte ich es für verständlicher, den Prozess zu beschreiben, als mehr Tabellen mit Zufallszahlen auf Sie zu stapeln. Also:


Stellen Sie sich vor, Sie sind ein Child . Sie werden gebeten, sich in einem neuen Stück Erinnerung zu konstruieren. Da Sie Grandparent direkt erben (was virtuelle Vererbung bedeutet), rufen Sie seinen Konstruktor direkt auf (wenn es keine virtuelle Vererbung wäre, würden Sie den Konstruktor Parent1 , der wiederum den Grandparent Konstruktor aufrufen würde). Sie setzen this += 32 Bytes, da sich hier Grandparent befinden und Sie den Konstruktor aufrufen. Sehr einfach.


Dann ist es Zeit, Parent1 zu Parent1 . Parent1 kann davon ausgehen, dass Grandparent zum Zeitpunkt seiner Erstellung bereits erstellt wurde, sodass er beispielsweise auf Daten und Methoden von Grandparent zugreifen kann. Aber warte, wie kann er wissen, wo er diese Daten findet? Sie befinden sich nicht am selben Ort wie die Variablen Parent1 !


Die construction table for Parent1-in-Child betritt die Szene. In dieser Tabelle wird Parent1 wo Daten zu finden sind, auf die Parent1 kann. this verweist auf die Daten von Parent1 . virtual-base offset gibt an, wo Sie Großeltern-Daten finden können: Schritt 32 Bytes vorwärts, und Sie finden Grandparent Speicher. Verstehst du es? Der Versatz für die virtuelle Basis ähnelt top_offset, gilt jedoch für virtuelle Klassen.


Parent2 wir das verstanden haben, ist der Aufbau von Parent2 im Grunde der gleiche, nur unter Verwendung der construction table for Parent2-in-Child . Tatsächlich hat Parent2-in-Child einen virtual-base offset der virtual-base offset von 16 Bytes.


Lassen Sie die Informationen ein wenig einweichen. Bist du bereit fortzufahren? Gut
Kommen wir nun zu VTT . Hier ist die VTT Struktur:


Die AdresseWertSymbolInhalt
0x4009a00x400950vtabelle für Kind + 24Parent1-Einträge in vtable Child
0x4009a80x4009f8Konstruktionstabelle für Parent1-in-Child + 24Parent1-Methoden in Parent1-in-Child
0x4009b00x400a18Konstruktionstabelle für Parent1-in-Child + 56Großelternmethoden für Parent1-in-Child
0x4009b80x400a98Konstruktionstabelle für Parent2-in-Child + 24Parent2-Methoden in Parent2-in-Child
0x4009c00x400ab8Konstruktionstabelle für Parent2-in-Child + 56`Großelternmethoden für Parent2-in-Child
0x4009c80x400998vtable für Kind + 96`Großelterneinträge in vtable Child
0x4009d00x400978vtable für Kind + 64`Parent2-Einträge in vtable Child

VTT steht für virtual-table table , was bedeutet, dass es sich um eine vtable handelt. Dies ist eine Übersetzungstabelle, die beispielsweise weiß, ob der Konstruktor Parent1 für ein einzelnes Objekt, für das Parent1-in-Child Objekt oder für Parent1-in-SomeOtherObject . Es wird immer unmittelbar nach vtable , damit der Compiler weiß, wo er es finden kann. Daher muss in den Objekten selbst kein weiterer Zeiger gespeichert werden.


Fuh ... viele Details, aber ich denke, wir haben alles abgedeckt, was ich abdecken wollte. Im vierten Teil werden wir uns mit den Details der übergeordneten vtables . Überspringen Sie nicht, da dies wahrscheinlich der wichtigste Teil in diesem Artikel ist!


Teil 4 - Vom Compiler generierter Code


An diesem Punkt in diesem Artikel haben wir erfahren, wie vtables und typeinfo in unsere Binärdateien passen und wie der Compiler sie verwendet. Jetzt werden wir den Teil der Arbeit verstehen, den der Compiler automatisch für uns erledigt.


Konstruktoren


Für den Konstruktor einer Klasse wird der folgende Code generiert:


  • Aufrufen von übergeordneten Konstrukten, falls vorhanden;
  • Festlegen von vtable-Zeigern, falls vorhanden;
  • Initialisierung der Mitglieder gemäß der Liste der Initialisierer;
  • Code-Ausführung in Klammern des Konstruktors.

All dies kann ohne expliziten Code geschehen:


  • Übergeordnete Konstruktoren werden standardmäßig automatisch gestartet, sofern nicht anders angegeben.
  • Mitglieder werden standardmäßig initialisiert, wenn sie keinen Standardwert oder keine Einträge in der Initialisierungsliste haben.
  • Der gesamte Konstruktor kann markiert werden = default;
  • Nur die vtable-Zuweisung wird immer ausgeblendet.

Hier ist ein Beispiel:


 #include <iostream> #include <string> using namespace std; class Parent { public: Parent() { Foo(); } virtual ~Parent() = default; virtual void Foo() { cout << "Parent" << endl; } int i = 0; }; class Child : public Parent { public: Child() : j(1) { Foo(); } void Foo() override { cout << "Child" << endl; } int j; }; class Grandchild : public Child { public: Grandchild() { Foo(); s = "hello"; } void Foo() override { cout << "Grandchild" << endl; } string s; }; int main() { Grandchild g; } 

Schreiben wir einen Pseudocode für den Konstruktor jeder Klasse:


ElternteilKindEnkelkind
1. vtable = vtable Parent;1. Ruft den Standardkonstruktor Parent auf.1. Ruft den Standardkonstruktor Child auf.
2. i = 0;2. vtable = vtable Child;2. vtable = vtable Enkelkind;
3. Ruft Foo () auf;3. j = 1;3. Ruft den Standardkonstruktor auf;
4. Ruft Foo () auf;4. Ruft Foo () auf;
5. Ruft den Operator = für s auf;

Angesichts dessen ist es nicht verwunderlich, dass sich vtable im Kontext des Klassenkonstruktors auf die vtable dieser Klasse selbst und nicht auf ihre spezifische Klasse bezieht. Dies bedeutet, dass virtuelle Anrufe so aufgelöst werden, als ob keine Erben verfügbar wären. So die Schlussfolgerung


 Parent Child Grandchild 

Was ist mit rein virtuellen Funktionen? Wenn sie nicht implementiert sind (ja, Sie können rein virtuelle Funktionen implementieren, aber warum brauchen Sie das?), Werden Sie wahrscheinlich (und hoffentlich) direkt zu segfault übergehen. Einige Compiler vernachlässigen den Fehler, was cool ist.


Destruktoren


Wie Sie sich vorstellen können, verhalten sich Destruktoren wie Konstruktoren, nur in umgekehrter Reihenfolge.


Hier ist eine kurze Übung zum Nachdenken: Warum ändern Destruktoren den vtable-Zeiger so, dass er auf seine eigene Klasse zeigt, anstatt einen Zeiger auf eine bestimmte Klasse zu lassen? Antwort: Zum Zeitpunkt des Starts des Destruktors war jede erbende Klasse bereits zerstört. Das Aufrufen von Methoden dieser Klasse ist nicht das, was Sie tun möchten.


Implizite Besetzung


Wie wir im zweiten und dritten Teil gesehen haben , entspricht ein Zeiger auf ein untergeordnetes Objekt nicht unbedingt dem übergeordneten Zeiger derselben Instanz (wie im Fall der Mehrfachvererbung).


Für Sie (den Entwickler) gibt es jedoch keine zusätzliche Arbeit zum Aufrufen einer Funktion, die einen übergeordneten Zeiger empfängt. Dies liegt daran, dass der Compiler dies implizit verschiebt, wenn Sie Zeiger und Verweise an übergeordnete Klassen anhängen.


Dynamische Besetzung (RTTI)


Dynamische Casts verwenden typeinfo Tabellen, die wir im ersten Teil untersucht haben. Sie tun dies zur Laufzeit, indem sie den Eintrag typeinfo einen Zeiger vor dem Zeiger vtable und die Klasse von dort verwenden, um zu prüfen, ob die vtable möglich ist.


Dies erklärt die Kosten von dynamic_cast bei häufiger Verwendung.


Methodenzeiger


Ich plane, in Zukunft einen vollständigen Beitrag über Zeiger auf Methoden zu schreiben. Vorher möchte ich betonen, dass ein Zeiger auf eine Methode, die auf eine virtuelle Funktion verweist, tatsächlich eine überschriebene Methode aufruft (im Gegensatz zu Zeigern auf Funktionen, die keine Mitglieder sind).


 // TODO:  ,     

Überprüfe dich selbst!


Jetzt können Sie sich erklären, warum sich das folgende Codefragment so verhält wie es sich verhält:


 #include <iostream> using namespace std; class FooInterface { public: virtual ~FooInterface() = default; virtual void Foo() = 0; }; class BarInterface { public: virtual ~BarInterface() = default; virtual void Bar() = 0; }; class Concrete : public FooInterface, public BarInterface { public: void Foo() override { cout << "Foo()" << endl; } void Bar() override { cout << "Bar()" << endl; } }; int main() { Concrete c; c.Foo(); c.Bar(); FooInterface* foo = &c; foo->Foo(); BarInterface* bar = (BarInterface*)(foo); bar->Bar(); //  "Foo()" - WTF? } 

Damit ist mein vierteiliger Artikel abgeschlossen. Ich hoffe du lernst etwas Neues, genau wie ich.

Source: https://habr.com/ru/post/de480610/


All Articles