Standardkonformer Hack


  • Mod

    camper schrieb:

    Inwiefern?

    Du deklarierst ein Array der Größe 1. Es hat 1 Element. Der Ausdruck elements[7] o.ä. resultiert demnach in undefiniertem Verhalten, völlig unabhängig davon, wie viel Speicher du reservierst - weil das per Definition äquivalent ist zu *(elements + 7) , und dieser Ausdruck wiederum eine Addition enthält die laut dem genannten Standardparagraph UB erzeugt - weil der Ursprüngliche Zeiger, das decay-te elements , und der resultierende Zeiger, elements + 7 , nicht auf Elemente desselben Arrays verweisen.

    Die betreffende reservierte Speicherstelle bekommt ja nicht auf magische Weise den effektiven Typ Vector (in Abwesenheit einer Zuweisung oder eines placement-new).

    Speicherstellen haben keine Typen. Objekte liegen an bestimmten, hintereinanderliegenden Bytes im Speicher; und die Lebenszeit eines Objektes beginnt sobald ... den Rest kennst du ja. placement-new ist hier demnach gar nicht nötig, schließlich hat Vector eine triviale Initialisierung.



  • Es gibt jedenfalls unter C eine Extension für genau diesen Anwendungsfall:
    https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html
    Mir ist jetzt nur nicht ganz klar, ob die auch für C++ gilt.



  • 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?

    ps:

    Arcoth schrieb:

    weil der Ursprüngliche Zeiger, das decay-te elements , und der resultierende Zeiger, elements + 7 , nicht auf Elemente desselben Arrays verweisen.

    Sondern?
    Erklär mal auf welches Array der eine und auf welches Array der andere zeigt.



  • Fällt jemandem ein Szenario ein in dem das schief gehen könnte?
    Ich hätte da höchstens die Idee eines hypothetischen Compilers der Bound checking bei vorhandener Größeninformation macht. Was aber vermutlich auch nicht konform wäre.



  • 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).


  • Mod

    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?


  • Mod

    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.


  • Mod

    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.


  • Mod

    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 Vector ist, oder?



  • Wieso sollte es ein Member von Vector sein müssen?


  • Mod

    hustbaer schrieb:

    Wieso sollte es ein Member von Vector sein müssen?

    Weil du darauf als Member zugreifst!? Der Member ist nämlich ein Array von Größe genau 1.


  • Mod

    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 foo und bar - 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.


  • Mod

    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.


  • Mod

    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 new verwenden? Produziert praktisch genau denselben Code.

    POD* p = new POD; // Nein, die Skalar-Member werden nicht initialisiert
    p->value = 123;
    

    Und bei malloc bin ich sehr skeptisch.
    Vergiss nicht: p muss 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 dieses char -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 malloc tatsä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 ein A ist, 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.


  • Mod

    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?


Anmelden zum Antworten