vTables bei Interfaces
-
Hallo
mich beschäftigte heute die Frage, wie vTables bei multiple inheritance aussehen. Dort habe ich auf der englischen Wikipedia (http://en.wikipedia.org/wiki/Virtual_method_table) eine eigentlich ganz gute Erklärung gefunden. Diese lautet (kurze Zusammenfassung des Artikels):
class B1 { public: void f0() {} virtual void f1() {} int int_in_b1; }; class B2 { public: virtual void f2() {} int int_in_b2; }; class D : public B1, public B2 { public: void d() {} void f2() {} // override B2::f2() int int_in_d; };Bei der oben dargestellten Konstellation hat D einfach 2 vtables, die direkt hintereinander liegen.
Das führt zu folgendem Problem:
D *d = new D(); B1 *b1 = dynamic_cast<B1*>(d); B2 *b2 = dynamic_cast<B2*>(d);Im Falle des Casts nach B2 würde der Pointer auf das das vTable von B1 zeigen. Und hier kommt meine Frage:
Im Artikel steht es werden Pointer Fixups durchgeführt, so dass b1 und b2 nicht auf die selben Adressen zeigen sondern auf "die richtige Adresse" die man für Objekte von B1 und B2 jeweils erwarteten würde. Leiter bleibt der Artikel die Erklärung schuldig, wie diese Fixups gemacht werden, bzw woher der Compiler weiss, wie die gemacht werden müssen. Und genau das würde ich gerne wissen.
Im oben genannten Beispiel könnte der Compiler meinetwegen noch das Wissen beim Cast für B2 über den Datentyp von d ausnutzen (das ist ja direkt vorher als D deklariert worden) um den Fixup machen zu können. Aber z.B. speziell wenn ich Interfaces verwendete weiss der Compiler an manchen Stellen in meinem Source nur, dass es das betreffende Objekt das Interface implementiert und sonst nichts weiter. Die Funktionsaufrufe funktionieren trotzdem, wie funktioniet das?

vielen dank und euch noch einen schönen Abend
-
also vorweg mal: der standard sagt nur was virtuelle funktionen sind, wie sie sich zu verhalten haben etc. von einem vtable sagt er nichts. genauso was dynamic_cast angeht wird keine implementierung vorgeschrieben, sondern nur ein verhalten.
was übliche implementierungen angeht...
der sog. "vtable-pointer" zeigt auf mehr als nur einen array aus funktions-zeigern für virtuelle funktionen. in wirklichkeit zeigt der zeiger auf eine datenstruktur, die generell informationen über die konkrete klasse enthält - nicht nur über deren virtuelle funktionen.
alles in worten zu erklären wäre etwas mühsam, aber vielleicht kannst du dir darunter was vorstellen:
struct type_info_impl { char const* m_name; // ... sämtliche informationen über einen konkreten typ, // inklusive sämtlicher basisklassen, deren offsets, access-restrictions (public/private etc.) }; struct vtable { type_info_impl* m_concrete_type; // <-- informationen über die konkrete klasse function_pointer m_virtual_functions[]; // function-pointer array, oder auch einfach ein "jump-table" (array aus "jmp F1; jmp F2; ..." befehlen) }; struct class_B1_impl { vtable* m_vtable; T1 m_first_B1_member; T2 m_second_B1_member; // ... }; struct class_B2_impl { vtable* m_vtable; T1 m_first_B2_member; T2 m_second_B2_member; // ... }; struct class_D_impl { class_B1_impl m_B1; // kein eigener vtable-pointer vor m_B2 nötig, da m_B2 bereits mit einem vtable-pointer anfängt class_B2_impl m_B2; // daher haben wir bei einfach-vererbung auch keinen zusätzlichen overhead T1 m_first_D_member; T2 m_second_D_member; // ... }; // was casts angeht ... der anfang einer dynamic_cast implementierung könnte ca. so aussehen: // NOTE: ein zeiger auf jedes objekt welches virtuelle funktionen hat, ist auch immer ein zeiger auf einen vtable-zeiger, // da ja jedes solche objekt mit einem vtable-zeiger anfängt. // daher können wir vtable** als "universellen" zeiger für polymorphe objekte verwenden vtable** dynamic_cast_impl( vtable** object, // zeiger auf das objekt type_info_impl* static_source_type, // typ des source-zeigers, damit wir wissen von wo aus wir "los-suchen" // (macht nen unterschied bei mehrfach-vererbung), // und damit wir wissen, wie wir den zeiger anpassen müssen type_info_impl* target_type // typ des destination-zeigers, damit wir wissen was wir suchen { if (!object) return 0; type_info_impl* concrete_object_type = (*object)->m_concrete_type; // typ des objekts // jetzt können wir die type_info_impl datenstrukturen durchklauben, um rauszubekommen, // ob der cast funktioniert, und wie wir den zeiger anpassen müssen // ... } // was aufrufen von funktionen angeht: eine beliebte variante ist, einfach "stub" funktionen zwischenzuschalten // quasi sowas: // eine B2 funktion. diese können wir direkt im B2 vtable für B2 instanzen eintragen. int some_B2_function(class_B2_impl* this_B2, int p1, int p2) { this_B2->m_first_B2_member = p1 + p2; return this_B2->m_second_B2_member; } // und im B2 vtable für D instanzen, tragen wir diesen stub ein: int some_B2_function_stub_for_Ds_vtable(class_D_impl* this_D, int p1, int p2) { // D* anpassen um daraus einen B2* auf's gleiche objekt zu machen // NOTE: reinterpret_cast auf char*, damit +1 auf den zeiger einen byte entspricht, // und nicht sizeof(class_D_impl) class_B2_impl* this_B2 = reinterpret_cast<class_B2_impl*>( reinterpret_cast<char*>(this_D) + offsetof(class_D_impl, m_B2)); // nu haben wir den passenden zeiger, und können an some_B2_function "weiterleiten" return some_B2_function(this_B2, p1, p2); }ich hoffe du kannst damit jetzt was anfangen

-
Ja vielen Dank, das erklärt einiges, was ich bisher nicht wusste. Auch wenn ich festgestellt habe, dass der Grund meiner initialen Frage nur ein Denkfehler meinerseits war und mich deine Antwort erst drauf gebracht hat
