union -> struct inside



  • Hi,
    ich habe folgenden Code:

    union myColor{
    struct{ //anonymer Struct
        unsigned char a,b,g,r; //falsch herum ist absicht!
    };
    unsigned int rgba;
    };
    
    int main(int argc, char** argv){
        myColor color;
        color.rgba = 0xFFC080AA;
        std::cout << +color.r << " " << +color.g << " " << +color.b << " " +color.a << std::endl; //+ bedeutet: als Zahl interpretieren
    
    }
    

    Wie weiß ich, dass im struct die verschiedenen a,b,g,r sind?.
    Ich weiß, dass das rückwärts wegen dem Big Endian ist.. aber wie kann er es in einem struct verpackt erkennen und zuordnen? So viel Intelligenz traue ich dem Compiler nicht zu 😉

    Vielen Dank für Antworten 🙂



  • Warum nimmst du an, dass hier Compilerintelligenz nötig wäre?
    Das Ganze ist undefiniertes Verhalten. Wenn die Bytes des unsigned richtig "unter" den Bytes der char liegen passt es halt.



  • @manni66
    Was ist hier deiner Meinung nach UB?
    Strict-Aliasing greift mMn. nicht weil Ausnahme für unsigned char . IB ist es natürlich, weil der Standard nicht festlegt wie die Bits in einem unsigned int angeordnet zu sein haben. Aber IB ist eben nicht UB.

    breakpoint schrieb:

    aber wie kann er es in einem struct verpackt erkennen und zuordnen? So viel Intelligenz traue ich dem Compiler nicht zu 😉

    Du traust dem Compiler nicht zu zu wissen dass er Variablen in POD-Structs in der Reihenfolge in den Speicher legt wie sie im Source-File stehen? Oder dass er bei Unions für jeden "Teil" der Union an der selben Adresse anfängt? Oder dass er bei Integers die grösser als ein Byte sind die native Byte-Order verwendet? Was traust du dem Compiler noch alles nicht zu, dass er 1 + 1 nicht zusammenzählen kann?

    Anders gesagt: der Compiler muss hier nix über die Byte-Order der Maschine wissen. Der Code wird sich auch hübsch reproduzierbar auf Little-Endian anders verhalten als auf Big-Endian. Davon abgesehen weiss der Compiler üblicherweise sehr genau was "seine" CPU so macht.
    Guggsdu: https://godbolt.org/g/u0cZan
    Sieh dir den Code an der für diese Funktionen generiert. Und dann überleg dir nochmal was du dem Compiler zutraust und was nicht. 🙂
    Ich persönlich finde das schon recht beeindruckend. Vor allem das letzte Beispiel ( quux ).



  • hustbaer schrieb:

    @manni66
    Was ist hier deiner Meinung nach UB?

    Der lesende Zugriff auf ein Member, dem nicht vorher ein Wert zugewiesen wurde.



  • * Der Speicher wurde initialisiert.
    * Der Zugriff erfolgt über nen unsigned char .
    Wäre es ein anderer Typ wie z.B. unsigned short wäre die Sache klar und du hättest natürlich Recht. So kann ich hier aber kein Problem sehen.

    Lesen per *reinterpret_cast<unsigned char*>(&color)[0] wäre definitiv OK. Ich denke dann sollte auch Lesen per color.a OK sein.

    Was sagen die Standard-Gurus? 😕



  • hustbaer schrieb:

    * Der Speicher wurde initialisiert.
    * Der Zugriff erfolgt über nen unsigned char .

    Es ist in obigem Code nicht sichergestellt, dass die Größe des structs gleich der Größe eines unsigned ints ist. Das klappt heute und der nächste Compiler meint vielleicht, Padding-Bytes einschieben zu müssen.
    Klar, einfach die Member auf 1 Byte ausrichten lasen, dann bliebe noch das Schreiben eines und Lesen des anderen Werts...

    Campers Meinung würde mich zwar auch interessieren, aber mit mit den passend definierten Masken zum Lesen der einzelnen Werte ist es doch auch nicht umständlicher und garantiert standardkonform, sodass der Code nicht wirklich nützlich ist und noch Fallstricke bereitstellt.



  • OK. Wenn unsigned int z.B. nur 16 Bit wäre, dann wäre es UB. Wohl wahr.
    Wenn sizeof(unsigned int) >= 4 , dann sehe ich hier kein UB. Es ist also sozusagen IB ob es UB ist oder nicht 😃

    nonpro schrieb:

    (...) mit den passend definierten Masken zum Lesen der einzelnen Werte ist es doch auch nicht umständlicher und garantiert standardkonform, sodass der Code nicht wirklich nützlich ist und noch Fallstricke bereitstellt.

    Klar, kein Einwand.



  • Wenn sizeof(unsigned int) > 4 ist (das = ist weg), dann funktioniert die union bei Big-Endian-Systemen nicht mehr.

    nonpro schrieb:

    ..., aber mit mit den passend definierten Masken zum Lesen der einzelnen Werte ist es doch auch nicht umständlicher und garantiert standardkonform, sodass der Code nicht wirklich nützlich ist und noch Fallstricke bereitstellt.

    Und der der Compiler macht daraus auch keinen langsameren Code.



  • Leider ist der Standard erstaunlich vage bei der Definition von Unions.
    Aber

    Standard schrieb:

    9.5 Unions [class.union]

    In a union, at most one of the non-static data members can be active at any time, that is, the value of at
    most one of the non-static data members can be stored in a union at any time.
    [Note:
    One special guarantee
    is made in order to simplify the use of unions: If a standard-layout union contains several standard-layout
    structs that share a common initial sequence (9.2), and if an object of this standard-layout union type
    contains one of the standard-layout structs, it is permitted to inspect the common initial sequence of any of
    standard-layout struct members; see 9.2.
    — end note]

    Es gibt also genau ein aktives Element, und auf die anderen darf nur in Ausnahmefällen zugegriffen werden.

    Siehe ebenfalls http://en.cppreference.com/w/cpp/language/union:

    ...
    it's undefined behavior to read from the member of the union that wasn't most recently written.


  • Global Moderator

    Ganz so einfach ist das nicht, manni66. hustbaer bezieht sich gerade auf diese Ausnahmefälle. Leider ist der Standard in dieser Hinsicht ziemlich schwer verständlich/ungeschickt formuliert. Die Intention des Standards ist vermutlich, dass es erlaubt sein sollte, denn in C ist es explizit erlaubt und man wollte beim Standard möglichst erreichen, dass C++ in Hinsicht des Speicherlayouts trivialer Objekte möglichst kompatibel zu C ist. Mit den ganzen Verklausulierungen des Standards ist am Ende aber meiner Einschätzung nach etwas heraus gekommen, das auf undefiniertes Verhalten in diesem Fall hinaus läuft.

    Man kann sich praktisch aber darauf herausreden, dass der GCC (und vermutlich alle bekannten Compiler) ganz explizit sagt, dass dies funktionieren wird.



  • Meiner Interpretation des Standard nach handelt es sich bei obigem Code um UB. Es wird da aus einem inaktiven Member einer union gelesen, was laut Standard UB ist und fertig. Die Ausnahme von wegen common initial sequence greift hier nicht...


  • Global Moderator

    dot schrieb:

    Meiner Interpretation des Standard nach handelt es sich bei obigem Code streng genommen um UB; aus dem einfachen Grund, dass da aus einem inaktiven Member einer union gelesen wird, was laut Standard UB ist. Die Ausnahme von wegen common initial sequence greift hier nicht...

    Und wo steht das im Standard? So einfach ist das wie gesagt nicht...



  • Kein Standardreiterei, aber Hinweis in eine Richtung, in die man recherchieren kann, wenn mans genau wissen will:

    Ich erinnere mich dass die Problematik in einem Vortrag zur Sprache kam (ich glaube es war einer der CppCon-Vorträge von Chandler Carruth über die Jahre).
    Dort wurde erwähnt, dass derlesende Zugriff auf einen nicht aktiven Member wie in diesem Fall tatsächlich UB ist. Allerdings sei dieses Pattern derart weit
    verbreitet - auch als Methode um vermeintlich strict-aliasing-konformen Code zu schreiben - dass hier wohl duch die Bank alle Compiler das "Richtige" machten.

    Dennoch, für wirklich standardkonformen Code bleibt einem nur entweder die Werte über Bit-Schubserei oder via memcpy herauszufischen. Letzteres sieht
    übrigens meist schlimmer aus als es ist: Die Compiler optimieren heutzutage gerade auch solche winzigen memcpy -Aufrufe recht gut - auch zu einem simplen
    MOV zwischen Registern wenn möglich (trotz des "mem" im Namen).

    P.S.: Ich glaube auch dass bezüglich Aliasing das von hustbaer vorgeschlagene reinterpret_cast in Ordnung wäre, allerdings nur aus dem Grund, dass ein
    8-Bit-Farbkanal "zufällig" den richtigen Datentyp hat. Ich würde das eher vermeiden wollen, da zumindest bei den Sachen die ich selbst so mache nicht nur
    8-Bit-Kanäle verwendet werden (und auch nicht immer für jeden Kanal gleich viele Bits).



  • Vergesst das aktive Element. Oder auch nicht, denn wenn es sich wie gesagt um etwas anderes als ( unsigned ) char handeln würde, dann wären diese Regeln natürlich wichtig -- und auch dieser Fall interessiert natürlich zu Recht viele Leute.

    Hier sind es aber eben unsigned char , und damit wird der Fall IMO viel etwas einfacher. Denn man darf *jeden* (initialisierten) Speicher als char oder unsigned char auslesen, ganz egal was sich dort für ein Objekt befindet. Ebenso darf man jeden Speicher (initialisiert oder nicht) mit char bzw. unsigned char überschreiben. Das Überschreiben eines Objekts mit char bzw. unsigned char zerstört dieses -- natürlich ohne dass dabei der Destruktor aufgerufen würde. (Was natürlich auch bedeutet: man darf das nur machen wenn es für das korrekte Funktionieren des Programms nicht nötig ist dass der Destruktor aufgerufen wird.)

    Weiters gibt es dann noch Sonderregeln, die es erlauben nach diesem "Überschreiben mit char " den Speicher wieder als ein Objekt eines anderen Typs zu verwenden -- so lange dieser andere Typ bestimmte Voraussetzungen erfüllt. Klassische C structs erfüllen z.B. diese Voraussetzungen. (Weiters sind speziell in C++11 noch etliche Dinge zusätzlich erlaubt, z.B. darf so ein struct IIRC nicht-virtuelle public Memberfunktionen haben etc. - aber die genauen Regeln dafür hab ich mir nie gemerkt.)

    Diese Regeln führen u.A. dazu dass man eine memcpy -artige Funktion selbst implementieren kann, und verwenden um damit beliebige simple "Daten-structs" zu kopieren, ohne dabei in UB (oder auch nur IB) hineinzulaufen.

    D.h. folgendes ist vollkommen definiert und vollkommen OK:

    #include <stdio.h>
    #include <stdlib.h>
    
    void MyMemCpy(void* dest, void const* src, size_t size)
    {
        unsigned char* d = static_cast<unsigned char*>(dest);
        unsigned char const* s = static_cast<unsigned char const*>(src);
        while (size--)
            *d++ = *s++;
    }
    
    struct Foo
    {
        int a;
        int b;
    };
    
    int main()
    {
        Foo const f1 = { 1, 2 };
        Foo* fp = static_cast<Foo*>(malloc(sizeof(Foo)));
        if (!fp)
            exit(1);
        MyMemCpy(fp, &f1, sizeof(Foo));
        printf("a = %d, b = %d\n", fp->a, fp->b);
        free(fp);
    }
    

    Wenn jetzt jemand argumentieren möchte, dass - unter der Voraussetzung dass sizeof(int) >= 4 * sizeof(char) , der Zugriff auf color.a UB sein soll, dann würde mich interessieren was der Unterschied zu meinem Beispiel sein soll. Also warum ist color.a Lesen verboten, wohingegen *s Lesen in meinem Beispiel OK ist.
    In beiden Fällen wird ein char gelesen "wo kein char ist". Wie man die Adresse ermittelt hat sollte dabei keine Rolle spielen. So lange sichergestellt ist dass die Adresse "passt" (also auf ein Byte zeigt welches initialisiert wurde), ist es erlaubt.



  • Was den "was wenn es kein (unsigned) char wäre?" Fall angeht: Hier glaube ich dass es tatsächlich UB wäre. Zumindest hab' ich das öfters gelesen, u.A. auch von Leuten wo ich den Eindruck hatte dass sie wissen wovon sie reden (schreiben).

    Und es macht mMn. auch Sinn. Denn wenn man sich die klassische "strict Aliasing" Problemfunktion anguckt...

    int Foo(int* a, short* b)
    {
        *a = 0;
        *b = 1;
        return *a;
    }
    

    Die meisten Compiler optimieren das return *a hier (berechtigterweise) zu einem return 0 . Weil strict aliasing ja besagt dass a und b unmöglich "aliasen" können (dürfen).
    Und was soll nun passieren wenn man folgendes macht

    #include <stdio.h>
    
    int Foo(int* a, short* b)
    {
        *a = 0;
        *b = 1;
        return *a;
    }
    
    union U
    {
        int a;
        short b;
    };
    
    int main()
    {
        U u;
        printf("Foo: %d\n", Foo(&u.a, &u.b));
    }
    

    Hier kommt mit optimiertem Code üblicherweise "Foo: 0" raus. Wenn es für enums ne Ausnahme (ohne weitere Einschränkungen) gäbe, dann müsste aber "Foo: 1" rauskommen. D.h. der Compiler dürfte in Foo nicht mehr das return *a zu return 0 optimieren. Was doof wäre, denn damit wäre die ganze strict aliasing Regel für die Katz. Was jetzt einige sicher freuen würde, aber das ist wieder ein anderes Thema -- ist halt einfach nicht so 🙂

    D.h. entweder es gibt ne Ausnahme die wieder super krass eingeschränkt ist, und das Beispiel hier eben irgendwie ausschliesst, oder es ist schlichtweg einfach grundsätzlich verboten. Ich gehe wie gesagt davon aus dass es grundsätzlich verboten ist -- bzw. mehr noch, ich hoffe dass es grundsätzlich verboten ist. Weil alles andere die so schon super komplizierten Regeln in diesem Bereich noch komplizierter machen würde.