Zuweisungen



  • blupblup schrieb:

    #define QUAD(x) ((x) * (x))
    int main()
    {
    	int x = 4;
    	int erg = QUAD(++x);
    }
    

    Schauen wir mal. Ein Makro ersetzt ja einfach nur etwas. Also steht da:

    int x = 4;
    	int erg = ((++x) * (++x));
    

    C++ garantiert nicht, wann ein Seiteneffekt (Änderung des Speicherbereichs) eintritt. D.h. der Operator ++ gibt dir schön den um 1 erhöhten Wert zurück. Aber niemand weiß, ob der neue Wert schon auf die Variable geschrieben wird. Also kann vieles passieren:

    int x = 4;
    	int erg = ((5) * (5));
    

    oder

    int x = 4;
    	int erg = ((6) * (6));
    

    oder

    int x = 4;
    	int erg = ((5) * (6));
    

    oder...

    C++ garantiert dir aber, dass ein Seiteneffekt spätestens nach einem sequence-point (google wikipedia) eingetreten ist. D.h. erst nach der Anweisung int erg = ((++x) * (++x)); kannst du sicher sein, dass x=6 ist.

    int quadrat(int x)
    {
        return x * x;
    }
    
    int main()
    {
        int y = 4;
        int erg2 = quadrat(++y); //erg2 = 25
    }
    

    Hier ist es GANZ anders. Ein Funktionsaufruf stellt bereits ein sequence-point dar. D.h es ist sicher, dass der Parameter x den Wert 5 hat. Und 5*5 ist nunmal 25. :p


  • Mod

    Ich habe mal eine kleine Übersicht zum ersten Problem erstellt:

    int erg = ((i = i + 1) * (i = i + 1) * (i = i + 1));
              ^^^^^^1^^^^^   ^^^^^2^^^^^   ^^^^^3^^^^^
    
    //                                              op       side effect
    int L0 = i;                // load(1)           (a)      
    int L1 = L0 + 1;           // add(1)            (b)                     (a)<(b) 1.9/15s.2
    i = L1;                    // store(1)          (c)      *              (b)<(c) 5.17/1s.4
    int L2 = L1;               // stored value(1)   (d)                     (c)<(d) 5.17/1s.4
    
    int L3 = i;                // load(2)           (e)
    int L4 = L3 + 1;           // add(2)            (f)
    i = L4;                    // store(2)          (g)      *
    int L5 = L4;               // stored value(2)   (h)
    
    int L6 = i;                // load(3)           (i)
    int L7 = L6 + 1;           // add(3)            (j)
    i = L7;                    // store(3)          (k)      *
    int L8 = L7;               // stored value(3)   (l)
    
    int L9 = L2 * L5;          // mul(1,2)          (m)                     (d)<(m)  (h)<(m)
    int L10 = L9 * L8;         // mul(12,3)         (n)                     (l)<(n)  (m)<(n)
    
    erg = L7;                  // store             (o)      *              (o)<(n)
    
    // < sequenced before
    // > sequenced after
    // | indeterminately sequenced 
    // ~ unsequenced (1.9/15s1)
    // # unsequenced UB (1.9/15s3)
    
    //   a   b   c   d   e   f   g   h   i   j   k   l   m   n   o
    //a      <   <   <   ~   ~   #   ~   ~   ~   #   ~   <   <   <
    //b   >      <   <   ~   ~   ~   ~   ~   ~   ~   ~   <   <   <
    //c   >   >      <   #   ~   #   ~   #   ~   #   ~   <   <   <
    //d   >   >   >      ~   ~   ~   ~   ~   ~   ~   ~   <   <   <
    //e   ~   ~   #   ~      <   <   <   ~   ~   #   ~   <   <   <
    //f   ~   ~   ~   ~   >      <   <   ~   ~   ~   ~   <   <   <
    //g   #   ~   #   ~   >   >      <   #   ~   #   ~   <   <   <
    //h   ~   ~   ~   ~   >   >   >      ~   ~   ~   ~   <   <   <
    //i   ~   ~   #   ~   ~   ~   #   ~      <   <   <   ~   <   <
    //j   ~   ~   ~   ~   ~   ~   ~   ~   >      <   <   ~   <   <
    //k   #   ~   #   ~   #   ~   #   ~   >   >      <   ~   <   <
    //l   ~   ~   ~   ~   ~   ~   ~   ~   >   >   >      ~   <   <
    //m   >   >   >   >   >   >   >   >   ~   ~   ~   ~      <   <
    //n   >   >   >   >   >   >   >   >   >   >   >   >   >      <
    //o   >   >   >   >   >   >   >   >   >   >   >   >   >   >
    

    Der Begriff sequence point wird ja so nicht mehr verwendet.



  • dot schrieb:

    int erg = ((++x) * (++x));
    

    Es ist allerdings undefiniert, wann genau während der Auswertung dieses Ausdruckes der Inkrement passiert. Das einzige, was garantiert ist, ist, dass nach der Auswertung des ganzen Ausdruckes x um 2 inkrementiert wurde. Das Ergebnis kann daher also 16, 20, 24, 25, 30 oder 36 sein.
    Bei der Variante mit der Funktion gibt es das Problem nicht und das Ergebnis ist daher garantiert 25.
    Wenn du allerdings sowas hättest:

    void blub(int x, int y)
    {
      return x * y;
    }
    
    int x = 4;
    int erg = blub(++x, ++x);
    

    wär das Ergebnis wieder undefiniert...

    Achso verstehe. Das Beispiel mit der Funktion ist auch interessant, das hätte
    ich nicht erwartet.

    Also Aufrufe mit ++x sollte man bei Makros immer sein lassen.
    Wenn das Makro wie nachfolgend lauten würde könnte es dann sein,
    dass der Variablen erg manchmal der Wert 2 und manchmal der Wert 3 zugewiesen wird?
    Gäbe es also hier ein ähnliches Problem wie bei dem QUAD(X) Beispiel?

    #define BLA(x) (x)
    
    int main()
    {
        int a = 2;
        int erg = BLA(++a);
        return 0;
    }
    


  • blupblup schrieb:

    Also Aufrufe mit ++x sollte man bei Makros immer sein lassen.

    Nicht unbedingt, allerdings sind Makros sowieso zu vermeiden. Für sowas generell besser (inline) Funktionen verwenden.

    blupblup schrieb:

    Wenn das Makro wie nachfolgend lauten würde könnte es dann sein,
    dass manchmal der Wert 2 und manchmal der Wert 3 ausgegeben wird?
    Gäbe es also hier ein ähnliches Problem wie bei dem QUAD(X) Beispiel?

    #define BLA(x) (x)
    
    int main()
    {
        int a = 2;
        int erg = BLA(++a);
        return 0;
    }
    

    Nope, da kommt ganz sicher immer 3 raus. Das Problem hat nicht mit dem Makro an sich zu tun, sondern damit, dass du mehrere Inkrements im selben Ausdruck stehen hast...



  • Okay verstehe.
    Danke für die Hilfe.



  • Merk dir einfach die Faustregel: Innerhalb eine Anweisung darf eine Variable max. 1x modifiziert werden. Hälst du dich daran, bist du stets auf der sicheren Seite.



  • Genau wie out sagt, denn genau deshalb ist

    a = f(i++,i);
    

    auch wohl definiert!



  • Decimad schrieb:

    Genau wie out sagt, denn genau deshalb ist

    a = f(i++,i);
    

    auch wohl definiert!

    Nope, ist es nicht, die Auswertungsreihenfolge der Parameter ist nicht definiert. Es ist lediglich garantiert, dass der eine vor dem anderen ausgewertet wird, aber nicht, welcher zuerst.


  • Mod

    out schrieb:

    int x = 4;
    	int erg = ((++x) * (++x));
    

    [...] D.h. erst nach der Anweisung int erg = ((++x) * (++x)); kannst du sicher sein, dass x=6 ist.

    nein. Wenn der Compiler gut optimiert, kommt wahrscheinlich x==5 raus - mit UB ist es allerdings sinnlos, auf irgendein konkretes Ergebnis zu wetten.

    out schrieb:

    Merk dir einfach die Faustregel: Innerhalb eine Anweisung darf eine Variable max. 1x modifiziert werden. Hälst du dich daran, bist du stets auf der sicheren Seite.

    Das vermeidet eine Stolperfalle, hinreichend für wohldefiniertes Verhalten ist das noch lange nicht.

    Decimad schrieb:

    Genau wie out sagt, denn genau deshalb ist

    a = f(i++,i);
    

    auch wohl definiert!

    Leider hat auch dieser Code undefiniertes Verhalten (ich nehme mal an, dass f eine Funktion oder eine Klasse mit geeignetem Konstruktor sein soll)



  • camper schrieb:

    out schrieb:

    Merk dir einfach die Faustregel: Innerhalb eine Anweisung darf eine Variable max. 1x modifiziert werden. Hälst du dich daran, bist du stets auf der sicheren Seite.

    Das vermeidet eine Stolperfalle, hinreichend für wohldefiniertes Verhalten ist das noch lange nicht.

    Hast du ein Beispiel parat, in dem eine Variable innerhalb einer Anweisung nur 1x modifiziert wird und es trotzdem zu UB kommt?



  • out schrieb:

    camper schrieb:

    out schrieb:

    Merk dir einfach die Faustregel: Innerhalb eine Anweisung darf eine Variable max. 1x modifiziert werden. Hälst du dich daran, bist du stets auf der sicheren Seite.

    Das vermeidet eine Stolperfalle, hinreichend für wohldefiniertes Verhalten ist das noch lange nicht.

    Hast du ein Beispiel parat, in dem eine Variable innerhalb einer Anweisung nur 1x modifiziert wird und es trotzdem zu UB kommt?

    i = j++;
    


  • out schrieb:

    camper schrieb:

    out schrieb:

    Merk dir einfach die Faustregel: Innerhalb eine Anweisung darf eine Variable max. 1x modifiziert werden. Hälst du dich daran, bist du stets auf der sicheren Seite.

    Das vermeidet eine Stolperfalle, hinreichend für wohldefiniertes Verhalten ist das noch lange nicht.

    Hast du ein Beispiel parat, in dem eine Variable innerhalb einer Anweisung nur 1x modifiziert wird und es trotzdem zu UB kommt?

    int y = x++ + x;
    

    Sone schrieb:

    i = j++;
    

    Wo genau versteckt sich das UB?



  • dot schrieb:

    out schrieb:

    camper schrieb:

    out schrieb:

    Merk dir einfach die Faustregel: Innerhalb eine Anweisung darf eine Variable max. 1x modifiziert werden. Hälst du dich daran, bist du stets auf der sicheren Seite.

    Das vermeidet eine Stolperfalle, hinreichend für wohldefiniertes Verhalten ist das noch lange nicht.

    Hast du ein Beispiel parat, in dem eine Variable innerhalb einer Anweisung nur 1x modifiziert wird und es trotzdem zu UB kommt?

    int y = x++ + x;
    

    Stimmt. Dann muss man die Faustregel etwas strengen auslegen: Zwischen zwei sequence-points darf eine modifiziere Variable höchstens 1x auftauchen. Dann ist man wirklich safe, oder?



  • Ich glaube, + ist kein Sequence Point, auch wenn der Begriff veraltet ist.

    Und schlagt mich, aber: camper, die Tabelle sieht echt super aus, aber was genau drückt eine Zelle aus, wie werden Zeile und Spalte genau miteinander vermengt? Sorry, wenn das da irgendwie draus hervorgeht. Wenn Dir die Frage zu doof ist, vergiss sie.



  • Eisflamme schrieb:

    Ich glaube, + ist kein Sequence Point, auch wenn der Begriff veraltet ist.

    Eben, ist es auch nicht.
    Ist der Begriff für C++98/03 auch veraltet? Dachte das gilt nur für de neuen Standard.


  • Mod

    Allgemein:
    Im Allgemeinen (sofern nichts anderes gesagt wird) ist die Auswertung von Ausdrücken ungeordnet (unsequenced) im Verhältnis zur Auswertung irgendwelcher anderer Ausdrücke (1.9/15 Satz 1)
    Sofern zwei Ausdrücke das gleiche Objekt modifizieren oder einer das Objekt modifiziert und der andere vom gespeicherten Wert des Objektes abhängt, ist das Verhalten undefiniert, wenn die Auswertung der beiden Ausdrücke nicht geordnet (sequenced) ist (1.9/15 Satz 3)

    Zum Glück gibt es nicht sehr viele (allgemeine) Regeln, die die Reihenfolge der Auswertung festlegen:

    1. die Operanden eines Operators werden ausgewertet, bevor das Ergebnis des Operators berechnet wird (1.9/15 Satz 2), also in

    (1+2)+(3+4)
    

    werden erst 1+2 und 3+4 ausgewertet, bevor die Gesamtsumme gebildet wird. Das ist im Prinzip intuitiv klar.
    Ausnahmen sind bei bestimmten Operatoren (&& || ?: ,) zu beachten.

    2. Ausdrücke, die zu verschiedenen Anweisungen gehören, werden in der Reihenfolge ausgewertet, in der die Anweisungen stehen (a.k.a ";" ist ein Sequenzpunkt) 1.9/14.

    3. Argumente für Funktionsaufrufe werden ausgewertet, bevor die Funktion aufgerufen wird (die Auswertung der Argumente untereinander ist aber - bis auf eine Ausnahme - unbestimmt) 1.9/15 Satz 4.

    4. (Teil-)Ausdrücke (einschließlich anderer Funktionsaufrufe), die zum gleichen vollständigen Ausdruck wie ein Funktionsaufruf gehören, werden in unbestimmter Weise entweder vor oder nach dem Funktionsaufruf ausgeführt (indeterminately sequenced) 1.9/15 Satz 5. (das bedeutet auch, das Funktionsaufrufe nicht überlappen).

    (Eine Regel mit kleinerer Nummer hat jeweils Vorrang). Das sind ungefähr die Regeln, die man sich als Anfänger aneignen sollte, um den häufigsten Stolperfallen aus dem Weg zu gehen.

    Ein paar Besonderheiten sind bei , && || ?: zu beachten - das ist bekannt.
    Die Zuweisung macht ein paar zusätzliche Garantien:
    - der Wert der Zuweisung wird erst bestimmt, nachdem das Objekt modifiziert wurde. Damit werden Mehrfachzuweisungen der Form

    a = b = 0;
    

    möglich.
    - Es ist garantiert, dass der Wert der Zuweisung mit dem in der Zuweisung gespeicherten Wert übereinstimmt im Verhältnis zu einem Funktionsaufruf im gleichen vollständigen Ausdruck:

    int foo(int& i) { return ++i; }
    int i = 0;
    int j = (i=0) + foo(i);
    

    j ist garantiert 1; nicht etwa evtl. 2.

    Bei List-Initialisierung werden die Initialisierer der einzelnen Elemente nacheinander ausgewertet. Das ist sogar dann der Fall (siehe Ausnahme zu 3.), wenn die List-Initialisierung zu einem Funktionsaufruf führt:

    struct foo { foo(int, int) };
    int x = 0;
    foo{ x++, x++ };  // ruft den Konstruktor mit den Argumenten 0, 1 auf
    foo( x++, x++ );  // ist undefiniert
    

    Wahrscheinlich habe noch ein oder 2 Spezialsfälle vergessen - die sind dann aber wahrscheinlich auch praktisch ohne große Bedeutung.


  • Mod

    out schrieb:

    Stimmt. Dann muss man die Faustregel etwas strengen auslegen: Zwischen zwei sequence-points darf eine modifiziere Variable höchstens 1x auftauchen. Dann ist man wirklich safe, oder?

    Wenn ein Ausdruck vom Wert einer Variablen abhängt und diese Variable im gleichen Ausdruck auch modifiziert wird, muss die Leseoperation entweder dazu dienen, den zu speicherenden neuen Wert zu bestimmen, oder aber direkt vom modifizierenden Ausruck abhängen (=benutze die Zuweisung direkt als Operand). (Das ist ziemlich genau die Regel, wie sie in C++03 formuliert wurde).

    int x = 1;
    x = x + x + x + x; // ist ok
    


  • camper schrieb:

    out schrieb:

    Stimmt. Dann muss man die Faustregel etwas strengen auslegen: Zwischen zwei sequence-points darf eine modifiziere Variable höchstens 1x auftauchen. Dann ist man wirklich safe, oder?

    Wenn ein Ausdruck vom Wert einer Variablen abhängt und diese Variable im gleichen Ausdruck auch modifiziert wird, muss die Leseoperation entweder dazu dienen, den zu speicherenden neuen Wert zu bestimmen, oder aber direkt vom modifizierenden Ausruck abhängen (=benutze die Zuweisung direkt als Operand). (Das ist ziemlich genau die Regel, wie sie in C++03 formuliert wurde).

    int x = 1;
    x = x + x + x + x; // ist ok
    

    Hätte nicht gedacht, dass es so schwierig ist eine gute Faustregel aufzustellen :D, darum lass ich es nun lieber. 😃



  • dot schrieb:

    Wo genau versteckt sich das UB?

    Nirgendswo, da hab ich mich vertan. 🙂

    Ich glaube, ich werde erstmal ein Stündchen brauchen bis ich Campers Post verstanden habe.


Anmelden zum Antworten