Standardkonformer Hack
-
Vorweg: dass ich zu dem Thema im Standard nachgesehen habe ist schon einige Zeit her. Es ist möglich dass mich meine Erinnerung betrügt oder ich was falsch verstanden habe.
----
Ich behaupte dass eine vernünftige Auslegung des Standards garantiert dass es funktionieren muss.
Dummerweise hab' ich aber auch den Eindruck dass der Standard diesbezüglich sehr sehr vage/implizit/lückenhaft ist.IIRC macht die Array-Zeiger-Additions-Regel auf die sich Arcoth bezieht bei POD-Arrays deren Speicher nicht über
new T[]besorgt wurde überhaupt keinen Sinn. Bzw. ist total unglücklich formuliert.Gemeint ist dass die Addition verboten ist, wenn dabei eine Adresse entsteht, die ausserhalb des Speicherblocks liegt in dem die ursprüngliche Adresse liegt.
Und mit "Speicherblock" meine ich einfach das Ding das irgendwo mal "am Stück" angefordert wurde, über malloc(), operator new, new POD[] - was auch immer.
Weil da dann u.U. kein Speicher gemappt ist, und manche CPUs es nicht mögen wenn man solche Adressen erzeugt und in so einem Fall einen Trap (Fault) erzeugen. Auch wenn man die Adresse eben nur erzeugt oder von einem Register in ein anderes schupft, ohne wirklich einen Load auf die Adresse zu machen.
(Und mit "gemeint ist" meine ich: das ist der *Grund* warum es diese Regel gibt.)Bezogen auf diese Regel ist also alles was innerhalb eines über malloc angeforderten Blocks liegt als "das selbe Array" anzusehen (passendes Alignment vorausgesetzt).
-
hustbaer schrieb:
Arcoth schrieb:
Die Lebenszeit eines POD-Objektes beginnt, wenn Speicher mit der richtigen Ausrichtung und Groesse dafuer alloziert wurde.
Wo findet man das im Standard?
3.8/1 - allerdings ist das für die aufgeworfene Frage irrelevant. Die Legalität von Zeigerarithmetik hängt nicht von der Lebensdauer von Objekten ab, sondern nur davon, dass entsprechender Speicher zur Verfügung steht.
struct foo { int x; int elems[1]; }; struct bar { int x; int elems[1000]; }; ... foo* p = (foo*)std::malloc(sizeof(bar)); p->elems[100] = 42; // (1) reinterpret_cast<bar*>(p)->elems[100] = 42; // (2) p->elems < reinterpret_cast<bar*>(p)->elems; // (3)@Arcoth: UB oder nicht?
-
Wo findet man das im Standard?
§3.8 schrieb:
The lifetime of an object of type T begins when:
— storage with the proper alignment and size for type T is obtained, and
— if the object has non-trivial initialization, its initialization is complete.Gemeint ist dass die Addition verboten ist, wenn dabei eine Adresse entsteht, die ausserhalb des Speicherblocks liegt in dem die ursprüngliche Adresse liegt.
Und mit "Speicherblock" meine ich einfach das Ding das irgendwo mal "am Stück" angefordert wurde, über malloc(), operator new, new POD[] - was auch immer.If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined.
Erklär mal auf welches Array der eine und auf welches Array der andere zeigt.
Darum geht es gar nicht. Sie können beide gar nicht auf dasselbe Array-Objekt zeigen, weil die entsprechenden Membervariablen (
elements) nur ein Element halten.
-
camper schrieb:
struct foo { int x; int elems[1]; }; struct bar { int x; int elems[1000]; }; ... foo* p = (foo*)std::malloc(sizeof(bar)); p->elems[100] = 42; // (1) reinterpret_cast<bar*>(p)->elems[100] = 42; // (2) p->elems < reinterpret_cast<bar*>(p)->elems; // (3)@Arcoth: UB oder nicht?
Soweit ich sehen kann, ja. (1) ist der genannte Additionsfall.
(2) ist ein Aliasverstoß.
(3) ist noch ein Aliasverstoß.
Edit: Lass mich mal nachdenken.
-
§3.8 schrieb:
The lifetime of an object of type T begins when:
— storage with the proper alignment and size for type T is obtained, and
— if the object has non-trivial initialization, its initialization is complete.Dann entstehen bei malloc(1000) auch gleichzeitig alle int-Arrays die in dem von malloc() zurückgegebenen Block (passend aligned) Platz haben.
Du musst dir also nur ein passendes Array aussuchen das beide Elemente enthält.
-
Du musst dir also nur ein passendes Array aussuchen das beide Elemente enthält.
Funktioniert das tatsächlich?
Weil nicht jedes Array von beliebiger Größe ein Member von
Vectorist, oder?
-
Wieso sollte es ein Member von
Vectorsein müssen?
-
hustbaer schrieb:
Wieso sollte es ein Member von
Vectorsein müssen?Weil du darauf als Member zugreifst!? Der Member ist nämlich ein Array von Größe genau 1.
-
Ich habe mir das jetzt durch den Kopf gehen lassen, und bin zu dem Schluss gekommen, dass
malloc( sizeof(bar) )überhaupt kein Objekt erzeugt.
U.a. weil nach der Logik der Lebenszeiten die ich genannt habe, zwei Objekte - vom Typ
fooundbar- an der zurückgegebenen Adresse zu leben anfangen müssten. Aber auch weil allozierter Speicher ja nur allozierter Speicher ist; Objekte haben Typen, aber wo wurde das Objekt erzeugt - und damit sein Typ festgelegt?Stattdessen halte ich mich mal an
An object is created by a definition (3.1), by a new-expression (5.3.4) or by the implementation (12.2) when needed.
Die zitierte Regel aus §3.8 trifft demnach nur auf obige Objekte zu.
-
Das ist dann aber doof, weil wenn da kein Objekt existiert, dann dürfen wir auch nicht drauf zugreifen, nen?
Das kanns aber auch nicht sein, denn der Standard sagt ja dass man PODs nicht extra irgendwie hoch
new()en muss.Lass mich mich zitieren...
hustbaer schrieb:
Dummerweise hab' ich aber auch den Eindruck dass der Standard diesbezüglich sehr sehr vage/implizit/lückenhaft ist.
-
Das kanns aber auch nicht sein, denn der Standard sagt ja dass man PODs nicht extra irgendwie hoch
new()en muss.Wo?
Edit: Aso. Nein, der Standard redet da über Lebenszeit. Und von trivialen Defaultkonstruktoren. Hat mit Existenz primär nichts zu tun.
-
Arcoth schrieb:
Das kanns aber auch nicht sein, denn der Standard sagt ja dass man PODs nicht extra irgendwie hoch
new()en muss.Wo?
Keine Ahnung. Wo auch immer es steht.
Muss aber sein, um auch nur minimale Kompatibilität mit C zu gewährleisten.
PODs kann man einfach erzeugen in dem man über nen passend alignten und passend mit Speicher hinterlegten Zeiger reinschreibt.Wie sonst sollte
POD* p = (POD*)malloc(sizeof(POD)); p->value = 123;erlaubt sein?
Weiters muss es mMn. sogar möglich sein PODs über memcpy zu erzeugen. Wobei sich dann die Frage stellt: welches Objekt entsteht da? Der Originaltyp von dem die "Input-Bytes" stammen ist an der Stelle wo memcpy aufgerufen wird ja nicht bekannt.
Arcoth schrieb:
Nein, der Standard redet da über Lebenszeit. Und von trivialen Defaultkonstruktoren. Hat mit Existenz primär nichts zu tun.
Wie Lebenszeit mit Existenz nix zu tun haben kann musst du mir jetzt erklären.
----
Damit man auf ein Objekt zugreifen kann, muss es existieren, right?
Dann hätte ich jetzt die Frage: wann und wie entsteht ein POD, und wann und wie stirbt er wieder?
Die Frage ist u.A. wichtig für strict aliasing.
Beispiel:struct A { int x; float f; }; struct B { int x; double d; }; void Fun() { void* p = allocate_suitable_storage<A, B>(); A* a = static_cast<A*>(p); a->x = 123; a->f = 123; B* b = static_cast<B*>(p); // Bis hierher MUSS es mMn. noch OK sein. *a muss hier auf jeden Fall noch ein A "sein" (=ohne UB als A verwendbar sein), // wobei sämtliche Member dieses *a einen definierten Wert haben und ebenfalls angesprochen werden dürfen. int i = b->x; // OK? a->x++; // Auch noch OK? A a2 = *a; // Auch noch OK? b->d = 42; // Auch noch OK? B b2 = *b; // Auch noch OK? a->f = 32; // Auch noch OK? A a3 = *a; // Immer noch OK? }Ab wo beginnt hier UB, und warum (falls überhaupt)? Ich blick' da ehrlich gesagt nicht ganz durch.
mMn. müsste das aber alles OK sein.
-
Wie Lebenszeit mit Existenz nix zu tun haben kann musst du mir jetzt erklären.
Vergiss es, das ist Unsinn. Das Objekt fängt an zu existieren, wenn es zu leben anfängt - bevor es lebt spricht auch der Standard nur von "dem Speicher den das Objekt occupien wird" usw.

erlaubt sein?
Zuallererstmal: Warum nicht einfach
newverwenden? Produziert praktisch genau denselben Code.POD* p = new POD; // Nein, die Skalar-Member werden nicht initialisiert p->value = 123;Und bei
mallocbin ich sehr skeptisch.
Vergiss nicht:pmuss auf ein lebendes Objekt verweisen! Aber wo wurde es erzeugt?
Ich meine,char arr[100];erzeugt doch auch kein
POD?Weiters muss es mMn. sogar möglich sein PODs über memcpy zu erzeugen.
Das stimmt. Man kann ein POD-Objekt rüberkopieren in ein anderes POD-Objekt. Man kann ein POD-Objekt in ein
char-Array kopieren, und dieseschar-Array in ein POD-Objekt.Also ich bin nun verwirrt. Habe heute geträumt, ich würde mit meiner Friseurin beim Haare schneiden über den Standard reden.

Ab wo beginnt hier UB, und warum (falls überhaupt)?
Die Frage ist ja: Wenn
malloctatsächlich irgendwas erzeugt, welchen Typ hat es dann?
Du kannst nie auf ein Objekt vom Typ float mit einem glvalue vom Typ double zugreifen. Nie. Deswegen ist das ein strikter Aliasverstoß in Zeile 19.
(Wenn wir mal sagen dass das ins Leben gerufene Objekt einAist, was auch erst begründet werden müsste um die ersten paar Zeilen zu rechtfertigen)Den Rest schaue ich mir später an.
-
Ich denke die Lifetime von a beginnt in Zeile 7, sobald der Speicher als A-Speicher interpretiert wird. Eigentlich sollte da ein Konstruktoraufruf stattfinden, aber da man bei PODs ja afaik auch gefahrllos den Destruktoraufruf vor dem free weglassen kann, vermute ich jetzt einfach mal, dass dasselbe für den Konstruktor gilt.
Genau aus dem selben Grund beginnt die Lifetime von b in Zeile 10. Da aber keine zwei Objekte die selbe Adresse haben können, muss die von a logischerweise enden. Demzufolge ist es UB danach noch auf a zuzugreifen.
Und würde man danach a erneut den neuinterpretieren Speicher zuweisen beginnt wieder a zu leben und b hört auf. Der Zustand von a ist dann allerdings nicht definiert.
Soweit würde ich jetzt die Regeln interpretieren.
-
Nathan schrieb:
Eigentlich sollte da ein Konstruktoraufruf stattfinden, aber da man bei PODs ja afaik auch gefahrllos den Destruktoraufruf vor dem free weglassen kann, vermute ich jetzt einfach mal, dass dasselbe für den Konstruktor gilt.
joa, bei einem POD (welches ja einen trivialen
Defaultkonstruktor hat) muss dieser nicht aufgerufen werden.Soweit würde ich jetzt die Regeln interpretieren.
aeh, welche Regel genau interpretierst du da? Das ist keine Union. Also hast du einen Beleg?
Demzufolge ist es UB danach noch auf a zuzugreifen.
Denk jetzt ganz scharf nach: Wie hast du oben definiert, wann ein Objekt zu leben beginnt? Oder nur wenn man schreibt? Aber in beiden Faellen hast du ein glvalue auf ein A/B...
-
Arcoth schrieb:
Soweit würde ich jetzt die Regeln interpretieren.
aeh, welche Regel genau interpretierst du da? Das ist keine Union. Also hast du einen Beleg?
Na die Regeln über Lifetimes, §3.8 und das andere Zitat von dir.
Demzufolge ist es UB danach noch auf a zuzugreifen.
Denk jetzt ganz scharf nach: Wie hast du oben definiert, wann ein Objekt zu leben beginnt? Oder nur wenn man schreibt? Aber in beiden Faellen hast du ein glvalue auf ein A/B...
Ich habe definiert, dass ein POD zu leben beginnt wenn man einen Pointer auf einen entsprechend großen Memoryblock mit dem richtigen Alignment hat.
Wenn nun an der selben Stelle ein anderes POD anfängt zu leben, muss a aufhören zu leben, da es ja an der selben Stelle ist - wie bei Union. Demzufolge darf man nicht mehr auf a zugreifen. Ich versteh also nicht wo dein Problem liegt?
-
Arcoth schrieb:
Du kannst nie auf ein Objekt vom Typ float mit einem glvalue vom Typ double zugreifen. Nie.
Das steht nirgendwo. 3.10/10 bezieht sich nur auf den Zugriff auf den gespeicherten Wert eines Objektes. Ganz sicher verbietet dieser Absatz nicht, den Wert eines anderen Typs an diese Stelle zu schreiben (nat. setzt die Zuweisung wiederum voraus, dass an der Stelle dann auch bereits ein entsprechendes double-Objekt existiert - also müssen wird 3.8 auf eine Weise interpretieren, die tatsächlich die gleichzeitige Existenz von Objekten verschiedenen Typs an der gleichen Stelle erlaubt: kein Problem, da 3.8/1 überhaupt nicht darauf eingeht, auf welches T es sich eigentlich beziehen soll).
In dem Zusammenhang lesenswert #1116, #1530 womit auch klar ist, dass der Standard hier tatsächlich einige Unklarheiten enthält.
Ich empfehle auch den Blick in den C-Standard 6.5/6 bzgl. "effective type". Mir scheint, das sie dort etwas weiter sind, was die Behandlung unseres Problemes sind. Jede Interpretation des C++-Standards sollte meiner Meinung nach versuchen, im Ergebnis zu vergleichbaren Schlussfolgerungen zu kommen (weil wir wollen, dass Code, der sowohl zu C als auch zu C++ konform ist, sich in beiden Sprachen nach Möglichkeit gleich verhält).
-
@Nathan: Also, ich denke dass du Recht haben koenntest: Denn fuer PODs sagt der Standard ganz klar, dass ihre Lebenszeit u.a. endet wenn der Speicher in dem sie liegen reused wird.
Aber reusing heisst hier ganz klar ueberschreiben - nicht einen Zeiger darauf haben.Sonst...
std::string str; reinterpret_cast<int*>(&str)@hustbaer: Also wuerde ich sagen... ab der Zeile wo du den Speicherblock als B betrachtest und in die Membervariablen schreibst, wird dann der Speicher reused und die Lebenszeit vom A beendet.
Man darf nur keine lvalue-to-rvalue Konvertierung von a->f machen, solange ein B drin gespeichert ist, weil man dann strict-aliasing verletzen wuerde.
-
Ich antworte mal jetzt ohne dass ich alle vorherigen Beiträge gelesen habe.
Ein Fall in dem das ganze unter anderem genutzt wird, ist die Bitmap Verwaltung unter Windows: BITMAPINFO
Ist für mein Geschmack ein etwas verwirrendes und fehleranfälliges Konstrukt.
-
Arcoth schrieb:
Man darf nur keine lvalue-to-rvalue Konvertierung von a->f machen, solange ein B drin gespeichert ist, weil man dann strict-aliasing verletzen wuerde.
Doch, mMn. müsste das auch OK sein.
Und zwar in so einem Fall:a->x = 123; // Wir initialisieren ein A a->f = 42; b->x = 123; // Wir machen ne teilweise initialisierung eines B (ist ja erlaubt, auf b->d zuzugreifen hätte UB, aber das tu wir ja nicht) cout << a->f; // Wir lesen einfach einen float. Über einen A-Zeiger, aber das sollte egal sein (siehe unten)3.10.10
If a program attempts to access the stored value of an object through a glvalue of other than one of the
following types the behavior is undefined:
...
an aggregate or union type that includes one of the aforementioned types among its elements or nonstatic
data members (including, recursively, an element or non-static data member of a subaggregate
or contained union),Man bemerke das "an". Da steht nicht dass es sich dabei um einen speziellen Aggregate-Type handeln muss.
Für mich heisst das: strict aliasing greift nur für non-aggregates, non-unions und atomare Typen.
Sämtliche Aggregate-Layer zwischen einem Zeiger/einer Referenz und dem atomaren Typ müssten dabei egal sein. Ich dürfte also auch über einen C-Zeiger auf den float zugreifen, wenn der float da drin den selben Offset hat.EDIT: Fehler korrigiert
ps:
Im Grund genommen würde das bedeuten dass es für UDT aggregates keinen "effective type" bzw. "dynamic type" gibt, sondern nur für die darin enthaltenen atomaren Member. Und dann wäre die Sache ganz einfach, dann geht es einfach um den atomaren Typ mit dem eine Speicherstelle das letzte mal geschrieben wurde.