Polymorphes Objekt in "untypisiertem" Speicherbereich
-
Hallo da.
Folgende Situation: Ich bekomme von einer API einen Speichbereich in "beliebiger" Größe, den ich eigentlich nutzen kann wie ich will. Was ich darin nun ablegen "muss", ist ein polymorphes Objekt. Sodass ich, wenn ich an anderer Stelle wieder einen Zeiger auf diesen Speicherbereich bekomme, irgendwie einen Zeiger auf die gemeinsame Basisklasse erhalte.
Nun kann man ja nicht erwarten, dass wenn ich per placement-new eine Instanz einer konreten Instanz am Anfang des Puffers erzeuge, unbedingt die gewollte Basisklasse am "Anfang" des Puffers landet, ich also reinterpret_cast<Basisklasse*>(puffer_ptr) sicher machen kann. Aber ich möchte nun auch nicht unbedingt am Anfang des Bereichs einen Zeiger auf die Basisklasse ablegen müssen. Andererseits weiß ich aber genau, welche Klassen darin landen können, und ich habe Kontrolle über alle. Wenn sie also alle nur einfache Ableitungen haben, kann man mit einiger Sicherheit sagen, dass der Cast dennoch so ziemlich überall klappen würde?Viele Grüße
-
decimad schrieb:
.
Nun kann man ja nicht erwarten, dass wenn ich per placement-new eine Instanz einer konreten Instanz am Anfang des Puffers erzeuge, unbedingt die gewollte Basisklasse am "Anfang" des Puffers landetDoch, kannst du! Die Basisklassen liegen am Anfang des Objekts. Im Falle von Mehrfachvererbung eine nach der anderen.
, ich also reinterpret_cast<Basisklasse*>(puffer_ptr) sicher machen kann.
Und wenn du einen static_cast benutzt, dann würde das sogar automatisch sicher klappen. Siehe und staune:
#include <iostream> struct Base1 { int a; }; struct Base2 { int b; }; struct Derived : public Base1, public Base2 { void print_address() { std::cout << "Myself: " << this << '\n' << "My Base1: " << &a << '\n' << "My Base2: " << &b << '\n'; } }; int main() { Derived d; d.print_address(); Derived *p = &d; std::cout << "Derived pointer: " << p << '\n' << "Base1 pointer: " << static_cast<Base1*>(p) << '\n' << "Base2 pointer: " << static_cast<Base2*>(p) << '\n'; }(Irgendwie funktioniert ideone.com bei mir gerade nicht richtig
. Muss also selber übersetzt werden.) Mögliche Beispielausgabe:Myself: 0x7ffdc47ba240 My Base1: 0x7ffdc47ba240 My Base2: 0x7ffdc47ba244 Derived pointer: 0x7ffdc47ba240 Base1 pointer: 0x7ffdc47ba240 Base2 pointer: 0x7ffdc47ba244Wenn du untypisierten Speicher hast, musst du natürlich erst einmal irgendwie wissen, was das für ein Objekt ist, bevor du dann über die entsprechenden Casts die Adressen der Basisklassen erhalten kannst. Aber wahrscheinlich stellen sich diese Fragen gar nicht, denn so lange du keine Mehrfachvererbung hast, ist wie gesagt garantiert, dass die Basisobjektadresse mit der Objektadresse identisch ist.
-
Oh cool, normalerweise ist doch in C++ immer alles undefiniertes verhalten, was einfach an sich praktisch wäre!
Also wenn ich eine einfache Vererbungshierarchie habe, ist das gantiert, ansonsten kommt es auf die Reiheinfolge der Basisklassen in "Breadth-First" sozusagen an?
Danke für die Info, das stimmt mich glücklich!
-
Ich finde die Idee einen Zeiger (oder Offset) in den ersten Bytes abzuspeichern gar nicht so dumm.
- Zeiger auf den Block-Anfang holen, Platz für den Offset draufaddieren
std::alignaufrufen um für passendes Alignment für das zu erstellende Objekt zu sorgen- Mit dem nun passend alignten Zeiger placement new aufrufen
- Zeiger zur gewünschte Basisklasse casten
- Basisklassenzeiger mit
memcpyan den Anfang des Blocks kopieren
-
SeppJ schrieb:
decimad schrieb:
.
Nun kann man ja nicht erwarten, dass wenn ich per placement-new eine Instanz einer konreten Instanz am Anfang des Puffers erzeuge, unbedingt die gewollte Basisklasse am "Anfang" des Puffers landetDoch, kannst du! Die Basisklassen liegen am Anfang des Objekts. Im Falle von Mehrfachvererbung eine nach der anderen.
C++11 schrieb:
[class.derived]/4 The order in which the base class subobjects are allocated in the most derived object (1.8) is unspecified.
Aber immerhin hast du eine schöne empirische Bestätigung geliefert. In der Itanium ABI ist es nämlich garantiert
https://mentorembedded.github.io/cxx-abi/abi.html schrieb:
For each data component D (first the primary base of C, if any, then the non-primary, non-virtual direct base classes in declaration order, then the non-static data members and unnamed bitfields in declaration order), allocate as follows:
@hustbaer: Das sollte funktionieren.
-
decimad schrieb:
Also wenn ich eine einfache Vererbungshierarchie habe, ist das gantiert, ansonsten kommt es auf die Reiheinfolge der Basisklassen in "Breadth-First" sozusagen an?
Wie kommst du jetzt auf Breadth-First? Das von SeppJ beschriebene Verfahren ist Depth-first.
-
decimad schrieb:
Also wenn ich eine einfache Vererbungshierarchie habe, ist das gantiert, ansonsten kommt es auf die Reiheinfolge der Basisklassen in "Breadth-First" sozusagen an?
Ich muss es leider korrigieren: Es ist doch nicht garantiert, dass dieses genaue Layout vorliegt. Das heißt, die richtige Nutzung ist hier über static_cast oder dynamic_cast, die, wie von mir gezeigt, das Layout kennen und die Zeiger auf die richtige Weise verbiegen können. Implizite Casts funktionieren natürlich auch:
Derived *p = &d; Base1 *b1 = p; Base2 *b2 = p; std::cout << "Derived pointer: " << p << '\n' << "Base1 pointer: " << b1 << '\n' << "Base2 pointer: " << b2 << '\n';Liefert das gleiche Ergebnis wie mein vorheriges Beispiel.
citation needed schrieb:
Aber immerhin hast du eine schöne empirische Bestätigung geliefert. In der Itanium ABI ist es nämlich garantiert
https://mentorembedded.github.io/cxx-abi/abi.html schrieb:
For each data component D (first the primary base of C, if any, then the non-primary, non-virtual direct base classes in declaration order, then the non-static data members and unnamed bitfields in declaration order), allocate as follows:
Ahh, daher hatte ich das also. Ich war mir nämlich sicher, dass das irgendwo garantiert war, bevor ich dann selbstständig auf die von dir zitierte Stelle im C++-Standard stieß.
-
Stimmt, was SeppJ geschrieben hat, bedeutet nach etwas Überlegung für mich wirklich Depth-First (zumindest wenn man sich denkt, dass eine Klasse "zusammenhängend" mit ihren Basisklassen enthalten ist). Wenn ich es nicht erwähnt hätte, wäre es mir jetzt uach nicht klar geworden

Dass es doch nicht garantiert wird, ist wiederum ärgerlich. Jetzt kann ich wirklich den Zeigeransatz verfolgen, oder vielleicht besser komplett auf irgendeinen Polymorpheie-C++-Klassen-Firlefanz an der Stelle verzichten.
-
decimad schrieb:
Dass es doch nicht garantiert wird, ist wiederum ärgerlich.
Wieso? Du weißt doch anscheinend, was für ein Objekt das ist. Und wenn du das weißt, dann weiß der Compiler für dich die korrekten relativen Adressen der Basisobjekte. Bloß die Funktion eines reinterpret_cast ist nicht garantiert. So wie eigentlich immer, wenn reinterpret_cast im Spiel ist

-
Nein, ich weiß ja nicht, was für ein Objekt es ist, ich richte es nur so ein, das was auch immer da drin steckt, von einer bestimmten Klasse ableitet... das ist ja das zu lösende Problem. Wenn ich also von dem Speicherbereichzeiger nicht auf einen Basisklassen-Zeiger schließen kann, kann ich genauso gut ein Enum an den Anfang packen und mir die Basisklasse sparen, sondern direkt zu konkreten Klassen casten, basierend auf dem Enum.