POD base class und defined/undefined behaviour
-
Hallo,
ich habe grade folgende Klassen:
struct B //pod { friend class XY; //von XY auf flag zugegriffen private: void *flag; }; class D //kein pod : public B { public: D() { throw 1; } };Jetzt folgende Szenarien nach
D *buffer = static_cast< D* >( operator new( sizeof( D ) )1a:
buffer->B::flag = 0; void *f = buffer->B::flag; //ist f jetzt 0?1b:
B *b = buffer; b->flag = 0; void *f = b->flag; //wieder: f == 0?2:
buffer->B::flag = 0; try { new (buffer) D(); //throws } catch( ... ) { } //ist buffer->B::flag jetzt 0 (bzw würde es mit der Version aus 1b funktionieren)?
-
1a: Du kannst auf das flag nicht so zugreifen wie du es hier versuchst, da es private in B ist. Wenn es das nicht wäre, dann wäre f gleich 0 und das sollte garantiert sein. Denn wenn ich das richtig sehe, ist D vom Typ einer standard-layout class, ebenso B und beide sind layout-kompatibel mit einem void* und dann ist diese Aktion definiertes Verhalten.
1b: Gleiche Antwort und gleiche Begründung wie 1a.
2: Oh, das ist ein schwieriger Fall. Wenn ich den Standard richtig interpretiere, ist das Verhalten definiert. Das new sieht die Exception vom Konstruktor. Da du aber die Placement-Syntax benutzt hast, wird dann, im Gegensatz zum "normalen" new nichts freigegeben. Und da der Konstruktor am Objekt nichts gemacht hat, liegt an der Stelle noch genau das gleiche wie vorher und ist nachwievor gültig.
-
Nein. ::operator new liefert uninitialisierten Speicher. D hat einen Konstruktor, der für den allokierten Speicher nicht ausgeführt wurde. Den Speicher als ein D zu reinterpretieren sollte daher UB sein. Egal ob Standard Layout oder nicht.
Gilt für alle drei Beispiele. Ist alles eine Frage der object lifetime.
-
pumuckl schrieb:
Nein. ::operator new liefert uninitialisierten Speicher. D hat einen Konstruktor, der für den allokierten Speicher nicht ausgeführt wurde. Den Speicher als ein D zu reinterpretieren sollte daher UB sein. Egal ob Standard Layout oder nicht.
Gilt für alle drei Beispiele. Ist alles eine Frage der object lifetime.
Sehe ich das dann richtig, dass
B *b = buffer;
b->flag = 0;
void *f = b->flag;
//wieder: f == 0?nicht funktioniert, weil man D* nicht in B* umwandeln kann, wenn der Pointer auf kein gültiges D zeigt?
Eigentlich ist es ja statisch, wo B* relativ zu D* hinzeigen müsste, wenn man nicht virtual inheritance nutzt.
-
GorbGorb schrieb:
Sehe ich das dann richtig, dass
B *b = buffer;
b->flag = 0;
void *f = b->flag;
//wieder: f == 0?nicht funktioniert, weil man D* nicht in B* umwandeln kann, wenn der Pointer auf kein gültiges D zeigt?
Jup. Das ist genauso undefiniert, wie einem D* irgendeinen Phantasiewert zu geben und dann irgendetwas damit anzustellen. Dass dieser D* zufälligerweise auf einen Speicherbereich zeigt, der dir gehört und genügend Platz für ein D hätte, macht dabei keinen Unterschied. Dort ist kein D und damit ist alles, was mit einem D arbeitet, an dieser Stelle undefiniert. Das Einzige, was erlaubt ist, ist den D* auf einen void* zu casten und dann damit was anzustellen. Z.B. per Placement new dort ein D erzeugen.
-
C++11 schrieb:
3.8 Object lifetime [basic.life]
[...]5 Before the lifetime of an object has started but after the storage which the object will occupy has been allocated38 or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that refers to the storage location where the object will be or was located may be used but only in limited ways. For an object under construction or destruction, see 12.7. Otherwise, such a pointer refers to allocated storage (3.7.4.2), and using the pointer as if the pointer were of type void*, is well-defined. Such a pointer may be dereferenced but the resulting lvalue may only be used in limited ways, as described below. The program has undefined behavior if:
— the object will be or was of a class type with a non-trivial destructor and the pointer is used as the operand of a delete-expression,
— the pointer is used to access a non-static data member or call a non-static member function of the object, or
— the pointer is implicitly converted (4.10) to a pointer to a base class type, or
— the pointer is used as the operand of a static_cast (5.2.9) (except when the conversion is to void*, or to void* and subsequently to char*, or unsigned char*), or
— the pointer is used as the operand of a dynamic_cast (5.2.7).GorbGorb schrieb:
1a:
buffer->B::flag = 0; void *f = buffer->B::flag; //ist f jetzt 0?Undefiniert, Zugriff auf nichstatisches Member von D.
GorbGorb schrieb:
1b:
B *b = buffer; b->flag = 0; void *f = b->flag; //wieder: f == 0?Undefiniert, erfordert implizite Konvertierung.
GorbGorb schrieb:
2:
buffer->B::flag = 0; try { new (buffer) D(); //throws } catch( ... ) { } //ist buffer->B::flag jetzt 0 (bzw würde es mit der Version aus 1b funktionieren)?Undefiniert, entweder wegen 1a, oder, fall buffer so initialisiert wurde,
B *buffer = static_cast< B* >( operator new( sizeof( D ) );dann funktioniert zwar erst einmal die Initialisierung von flag. Nach der Konstruktion von D ist aber trotzdem alles unbestimmt: Der Konstruktor von D initialisiert B nicht explizit, welches folglich nur Defaultinitialisiert wird. Gleichzeitig ist ein Basisklassensubobjekt aber niemals ein POD-Objekt unabhängig vom Typ. Der Wert von flag ist damit selbst dann unbestimmt, wenn wir unterstellen, dass das B-Subobjekt an der gleichen Adresse wie das D-Objekt anfängt.
Das ist nat. nur unter der Annahme, dass die Exception vom D-Konstruktor stammt. Falls das placement-new fehlschlägt (kann nur bei Überladung passieren), käme es darauf an, ob die jeweilige Allokationsfunktion den Speicher überhaupt anfasst.
-
Danke, das war erhellend. Leider funktioniert dann die Lösung für ein Problem von mir nicht mehr:
Ich will einen Container< T > für Klassen, die (nicht virtuell) von B ableiten. B sieht etwa so aus:class B { friend class container; public: data &get_data() { return *data; } private: DataType *data; };In dem container sollen alle Objekte an einem Stück abgespeichert werden, es dürfen aber Lücken zwischen den einzelnen Elementen bestehen. Ich muss also irgendwie mit einem flag anzeigen, ob ein Platz besetzt ist. Dazu wollte ich B::data verwenden, da es erst im container gesetzt wird, nachdem ein Element konstruiert wurde (davor wird es nicht verwendet oder initialisert). Kann man sowas irgendwie standardkonform umsetzen?
Gehört zwar nicht dazu, aber wie kann man denn für einen Typ herausfinden, ob er einen nothrow move constructor hat? mein gcc 4.6 kennt
std::has_nothrow_copy_constructor, aber keinstd::has_nothrow_move_constructor
-
GorbGorb schrieb:
Danke, das war erhellend. Leider funktioniert dann die Lösung für ein Problem von mir nicht mehr:
Ich will einen Container< T > für Klassen, die (nicht virtuell) von B ableiten. B sieht etwa so aus:class B { friend class container; public: data &get_data() { return *data; } private: DataType *data; };In dem container sollen alle Objekte an einem Stück abgespeichert werden, es dürfen aber Lücken zwischen den einzelnen Elementen bestehen. Ich muss also irgendwie mit einem flag anzeigen, ob ein Platz besetzt ist. Dazu wollte ich B::data verwenden, da es erst im container gesetzt wird, nachdem ein Element konstruiert wurde (davor wird es nicht verwendet oder initialisert). Kann man sowas irgendwie standardkonform umsetzen?
Verstehe ich nicht, an einem Stück+Lücke scheint mir widersprüchlich zu sein. Und abgesehen von dem Initialisierungsproblem: wenn abgeleitete Klassen größer als die Basisklasse sein dürfen - wie stellst du sicher, dass ein B::data wirklich zu einem bestimten Platz gehört und nicht einfach durch ein großes Objekt, dass davor liegt, überschrieben wurde?
GorbGorb schrieb:
Gehört zwar nicht dazu, aber wie kann man denn für einen Typ herausfinden, ob er einen nothrow move constructor hat? mein gcc 4.6 kennt
std::has_nothrow_copy_constructor, aber keinstd::has_nothrow_move_constructorDas sollte is_nothrow_move_constructible sein. Ansonsten kannst du es auch nachbauen:
std::is_nothrow_constructible<T,T&&>::valueoder
noexcept(T(std::declval<T>()))
-
Stimmt, das war schlecht ausgedrückt: Es soll ein Container für nur einen Typ sein, der aber von B ableiten muss. Die einzelnen Plätze sind also alle gleich groß. Und zwischen den Elementen dürfen Lücken sein, aber alle liegen auf dem gleichen großen Stück Speicher.
-
Ich glaube nicht, dass es einen standardkonformen Weg gibt, das zu erreichen, wenn T nicht zu sehr eingeschränkt werden soll (man lönnte mit offsetof arbeiten, was einen standardlayout-Typen voraussetzt). Der Sinn der Ableitung erschliesst sich ohnehin nicht; da data private in B ist und kein Interface dafür deklariert wurde, beeinflusst es ja offenbar nicht das Verhalten von D.
(Vielleicht ja auch ein Fall für eine schlecht gestellte Frage: Statt zu fragen, wie A mit Hilfe von B gelöst werden kann, sollte gefragt werden wie A gelöst werden kann unter Hinweis auf die Probleme, die bei Versuch B bisher aufgetreten sind).
-
Jedes Objekt, das ich in dem container speichere, braucht Zugriff auf das zugehörige data Objekt (das gehört ihm aber nicht).
Ich werde es jetzt wohl so machen, dass ich einfach vor jedes Objekt einen Pointer speichere. Aus denen kann ich je eine linked list über freie und besetzte Stellen basteln, dann kann man wenigstens schneller iterieren und einfügen.EDIT:
Was mich in dem Zusammenhang mit Zeigerkonvertierungen nocht interessieren würde:
Wenn ich etwas habe wiestruct A { char m[100]; }; struct B { char m[100]; }; struct C : A , B { };und dann sowas mache:
C *c = get_ptr_c(); //könnte 0 sein B *b = c; A *a = c;Muss dann zur Laufzeit überprüft werden, ob c nicht null ist, um nur dann offsets zu addieren?