Ihre Frage ist interessant, aber ich fürchte, dass Sie mit Ihrer ersten Frage ein zu großes Ziel verfolgen, daher werde ich in mehreren Schritten antworten, wenn es Ihnen nichts ausmacht :)
Haftungsausschluss: Ich bin kein Compiler-Autor, und obwohl ich das Thema sicherlich studiert habe, sollte mein Wort mit Vorsicht genossen werden. Es wird Ungenauigkeiten geben. Und ich kenne mich mit RTTI nicht so gut aus. Da es sich hier nicht um einen Standard handelt, sind die von mir beschriebenen Möglichkeiten.
1. Wie wird die Vererbung umgesetzt?
Hinweis: Ich werde die Ausrichtungsprobleme weglassen, sie bedeuten nur, dass zwischen den Blöcken etwas Platz eingeplant werden könnte
Lassen wir virtuelle Methoden vorerst außen vor und konzentrieren uns darauf, wie die Vererbung implementiert wird, weiter unten.
Die Wahrheit ist, dass Vererbung und Komposition viel gemeinsam haben:
struct B { int t; int u; };
struct C { B b; int v; int w; };
struct D: B { int v; int w; };
aussehen werden:
B:
+-----+-----+
| t | u |
+-----+-----+
C:
+-----+-----+-----+-----+
| B | v | w |
+-----+-----+-----+-----+
D:
+-----+-----+-----+-----+
| B | v | w |
+-----+-----+-----+-----+
Schockierend, nicht wahr :) ?
Dies bedeutet jedoch, dass die Mehrfachvererbung recht einfach herauszufinden ist:
struct A { int r; int s; };
struct M: A, B { int v; int w; };
M:
+-----+-----+-----+-----+-----+-----+
| A | B | v | w |
+-----+-----+-----+-----+-----+-----+
Anhand dieser Diagramme wollen wir sehen, was passiert, wenn wir einen abgeleiteten Zeiger in einen Basiszeiger umwandeln:
M* pm = new M();
A* pa = pm; // points to the A subpart of M
B* pb = pm; // points to the B subpart of M
Verwenden Sie unser vorheriges Diagramm:
M:
+-----+-----+-----+-----+-----+-----+
| A | B | v | w |
+-----+-----+-----+-----+-----+-----+
^ ^
pm pb
pa
Die Tatsache, dass die Adresse von pb
unterscheidet sich geringfügig von dem der pm
wird durch Zeigerarithmetik automatisch vom Compiler für Sie behandelt.
2. Wie implementiert man virtuelle Vererbung?
Virtuelle Vererbung ist knifflig: Sie müssen sicherstellen, dass eine einzelne V
(für virtuelle) Objekte wird von allen anderen Unterobjekten gemeinsam genutzt. Definieren wir eine einfache Diamantenvererbung.
struct V { int t; };
struct B: virtual V { int u; };
struct C: virtual V { int v; };
struct D: B, C { int w; };
Ich werde die Darstellung weglassen und mich darauf konzentrieren, dass in einem D
Objekt, sowohl die B
y C
Unterteile teilen sich das gleiche Unterobjekt. Wie kann man das machen?
- Denken Sie daran, dass eine Klassengröße konstant sein sollte
- Denken Sie daran, dass weder B noch C beim Entwurf vorhersehen können, ob sie zusammen verwendet werden oder nicht.
Die Lösung, die gefunden wurde, ist daher einfach: B
y C
nur den Platz für einen Zeiger auf V
, und:
- wenn Sie eine eigenständige
B
ordnet der Konstruktor eine V
auf dem Heap, die automatisch behandelt werden
- wenn Sie bauen
B
als Teil einer D
die B
Unterabschnitt erwartet die D
Konstruktor, um den Zeiger auf den Speicherort von V
Und dasselbe gilt für C
natürlich.
En D
eine Optimierung, die es dem Konstruktor erlaubt, Platz zu reservieren für V
direkt im Objekt, denn D
erbt nicht virtuell entweder von B
o C
, was das von Ihnen gezeigte Diagramm ergibt (obwohl wir noch keine virtuellen Methoden haben).
B: (and C is similar)
+-----+-----+
| V* | u |
+-----+-----+
D:
+-----+-----+-----+-----+-----+-----+
| B | C | w | A |
+-----+-----+-----+-----+-----+-----+
Es ist zu beachten, dass das Gießen von B
a A
ist etwas komplizierter als einfache Zeigerarithmetik: Sie müssen den Zeiger in B
statt einfacher Zeigerarithmetik.
Es gibt aber auch einen schlimmeren Fall, das Up-Casting. Wenn ich Ihnen einen Verweis auf A
Woher wissen Sie, wie Sie zurück zum B
?
In diesem Fall wird die Magie ausgeführt von dynamic_cast
Dies erfordert jedoch eine gewisse Unterstützung (d. h. Informationen), die irgendwo gespeichert sind. Dies ist die so genannte RTTI
(Laufzeittyp-Informationen). dynamic_cast
wird zunächst feststellen, dass A
ist Teil einer D
und dann die Laufzeitinformationen von D abfragen, um zu erfahren, wo innerhalb von D
die B
Unterobjekt gespeichert ist.
Wenn wir in einem Fall wären, in dem es keine B
Unterobjekt, würde es entweder 0 zurückgeben (Zeigerform) oder ein bad_cast
Ausnahme (Referenzformular).
3. Wie implementiert man virtuelle Methoden?
Im Allgemeinen werden virtuelle Methoden durch eine v-Tabelle (d.h. eine Tabelle mit Zeigern auf Funktionen) pro Klasse und v-ptr zu dieser Tabelle pro Objekt implementiert. Dies ist nicht die einzig mögliche Implementierung, und es hat sich gezeigt, dass andere schneller sein könnten, aber es ist sowohl einfach als auch mit einem vorhersehbaren Overhead (sowohl in Bezug auf den Speicher als auch auf die Versandgeschwindigkeit).
Nehmen wir ein einfaches Objekt einer Basisklasse mit einer virtuellen Methode:
struct B { virtual foo(); };
Für den Computer gibt es so etwas wie Mitgliedschaftsmethoden nicht, also haben Sie in der Tat:
struct B { VTable* vptr; };
void Bfoo(B* b);
struct BVTable { RTTI* rtti; void (*foo)(B*); };
Wenn Sie von B
:
struct D: B { virtual foo(); virtual bar(); };
Sie haben nun zwei virtuelle Methoden, von denen eine die folgende überschreibt B::foo
, die andere ist brandneu. Die Computerdarstellung ist ähnlich wie:
struct D { VTable* vptr; }; // single table, even for two methods
void Dfoo(D* d); void Dbar(D* d);
struct DVTable { RTTI* rtti; void (*foo)(D*); void (*foo)(B*); };
Beachten Sie, wie BVTable
y DVTable
so ähnlich sind (da wir die foo
avant bar
) ? Das ist wichtig!
D* d = /**/;
B* b = d; // noop, no needfor arithmetic
b->foo();
Übersetzen wir den Aufruf in foo
in Maschinensprache (etwas):
// 1. get the vptr
void* vptr = b; // noop, it's stored at the first byte of B
// 2. get the pointer to foo function
void (*foo)(B*) = vptr[1]; // 0 is for RTTI
// 3. apply foo
(*foo)(b);
Diese vptrs werden von den Konstruktoren der Objekte initialisiert, wenn sie den Konstruktor von D
ist folgendes passiert:
D::D()
ruft auf. B::B()
in erster Linie seine Teilbereiche zu initiieren
B::B()
initialisieren vptr
auf seine vtable verweist, und gibt dann
D::D()
initialisieren vptr
auf seine vtable verweist und damit die von B
Deshalb, vptr
zeigte hier auf D's vtable, und damit auf die foo
angewendet wurde, war D. Für B
es war völlig transparent.
Hier teilen sich B und D die gleiche vptr!
4. Virtuelle Tabellen in der Multi-Vererbung
Leider ist diese gemeinsame Nutzung nicht immer möglich.
Erstens ist, wie wir gesehen haben, im Falle der virtuellen Vererbung das "gemeinsame" Element im endgültigen vollständigen Objekt seltsam positioniert. Es hat daher seine eigene vptr. Das ist 1 .
Zweitens ist im Falle der Mehrfachvererbung die erste Base auf das gesamte Objekt ausgerichtet, die zweite Base jedoch nicht (beide benötigen Platz für ihre Daten), weshalb sie ihre vptr nicht teilen kann. Das ist 2 .
Drittens, die erste Basis es auf das gesamte Objekt ausgerichtet und bietet uns damit dasselbe Layout wie im Falle der einfachen Vererbung (dieselbe Optimierungsmöglichkeit). Das ist 3 .
Ziemlich einfach, oder?