Standardkonformer Hack


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


  • Mod

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


  • Mod

    @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.


Anmelden zum Antworten