Auswertung in cout / innerhalb einer Zeile



  • Hallo Zusammen,

    an sich wollte ich nur kurz untersuchen wie ein void* inkrementiert wird, bin bei folgendem trivialen Beispiel aber auf etwas gestoßen was mich stutzig macht:

    void* vp{nulltpr};
    // entweder
    std::cout << vp++ << " / " << vp++ << " / " << vp++ << std::endl;
    // oder alternativ
    std::cout << ++vp << " / " << ++vp << " / " << ++vp << std::endl;
    

    Die erste Zeile gibt "0x2 / 0x1 / 0x0" zurück. Die zweite Zeile gibt "0x3 / 0x3 / 0x3" zurück. Soweit so gut, die Ausgabereihenfolge ist nicht definiert.

    Spinnt man die Sache noch etwas weiter:

    class SomeClass {
    public:
        SomeClass() {value = 0; }
    
        int increment() { return ++value; }
    
    private:
        int value;
    };
    
    int main() {
      SomeClass sc;
    
      std::cout << sc.increment() << " / " << sc.increment() << " / " << sc.increment() << std::endl;
    }
    

    Ist die Auswertereihenfolge (Ausgabe: 3 / 2 / 1) offensichtlich immer noch undefiniert.

    Dahe die Frage, ist die Auswertereihenfolge von Operationen innerhalb einer Anweisung immer undefiniert? Oder welche Regeln gelten hier?

    Vielen Dank!


  • Mod

    Die ganz genauen Regeln sind nicht so einfach zu erklären, aber grob kann man sagen:
    -Es gibt gewisse Punkte in einem Programm, an denen garantiert alles zwischen diesem Punkt und dem vorherigen ausgewertet wurde. Zu diesen Punkten zählen beispielsweise Semikolons. Der Operator<< zählt hingegen nicht dazu, daher ist die Auswertungsreihenfolge der Ausdrücke in deinen cout-Ketten nicht festgelegt.
    -Zwischen diesen Punkten ist die Auswertungsreihenfolge nicht festgelegt.
    -Wenn zudem noch mehrere Nebeneffekte das gleiche Objekt verändern, dann ist das ganze Verhalten sogar ausdrücklich undefiniert, das heißt es kann alles passieren. Diesen Fall hast du in deinen Beispielen, da dort das Objekt vp mehrmals verändert wird.
    -Die ganze obige Erklärung ist veraltet. So hat man das früher erklärt. Heute würde man sagen, dass bestimmte Operationen in bestimmte Klassen bezüglich der Auswertungsreihenfolge fallen. Aber das ist ohne eine komplette Liste, was in welche Klasse gehört, recht schwer anschaulich.



  • Kurz geasgt, das aller erster was mir auffällt ist, undefined, behaviour.



  • Hallo SeppJ,

    danke für deine Antwort. Eine Frage habe ich noch, wie ist der Sachverhalt denn dann bei folgendem Konstrukt?

    int a[]{0,1,2,3,4};
      int i{0};
      int v;
      v = a[i++]; std::cout << v << std::endl;
      v = a[i++]; std::cout << v << std::endl;
      v = a[i++]; std::cout << v << std::endl;
      v = a[i++]; std::cout << v << std::endl;
      v = a[i++]; std::cout << v << std::endl;
    

    Hier wird zwar brav 0 1 2 3 4 ausgegeben. Aber ist das jetzt nur Zufall, dass zuerst v = a[i] und danach i++ ausgeführt wird. Oder könnte es auch passieren, dass hier zuerst das Post-Inkrement i++ und anschließend die Zuweisung v = a[i] ausgeführt wird?

    Vielen Dank schon mal!



  • Nein.

    Du zeigst validen Code.

    Post-Inkrement gibt dir den alten Wert zurück.

    Das von vorher war undefined behaviour, weil.



  • guest84 schrieb:

    v = a[i++];

    postinkrement garantiert dir, dass sein nebeneffekt (das inkrementieren) erst nach dem zurückgeben des werts ausgeführt wird, und nachdem i nirgendwo sonst angefasst wird, ist das völlig korrekt.

    i = i++;

    ist schon eine kompliziertere frage, und eigentlich(!) (bis jetzt) undefiniert, weil zwar ++ garantiert, seinen nebeneffekt erst nach der rückgabe des werts durchzuführen, und das "=" garantiert, die zuweisung erst nach der auswertung der linken und rechten seite durchzuführen, allerdings nicht festgelegt ist, ob der nebeneffekt des ++ für die auswertung von a[i] schon eine rolle spielen darf oder nicht, und weil das nicht festgelegt ist, ist es automatisch undefiniertes verhalten (auch eine regel für diesen fall).

    das ganze ändert sich mit C++17 dann enorm, weil es dann eine liste der reihenfolgen gibt, in der auch nebeneffekte inkludiert sind, und die garantiert eingehalten wird.
    diese liste ist diese:

    (a vor b vor c vor d)

    a.b;
    a->b;
    a(b3, b1, b2); //es gibt zwar garantiert eine reihenfolge, aber es ist nicht festgelegt, welche
    a[b];
    
    //und auch:
    a << b;
    a >> b;
    a = b; //a += b, a *= c, etc.
    

    siehe P0145R3

    für dich als anwender wird es allerdings keinen großen unterschied machen, ob es nun undefiniert oder irgendwie definiert ist.

    BTW, was auch undefinierrt (aber nicht im gefährlichen sinn) ist, ist zeigerarithmetik auf void*. die in C++ eingebauten arithmetischen operatoren sind nur für zeiger auf einen vollständigen typ definiert, was void per definitionem nicht ist. (caste nach char*, wenn du zeiger-arithmetik mit bytes willst, aber generell stell dir die frage, ob zeigerarithmetik - oder void* - überhaupt nötig ist).


Anmelden zum Antworten