Unterschied zwischen Postfix und Präfix



  • Was passiert hier mit x? Es wird doch noch im 1. Durchgang inkrementiert, oder?

    #include <iostream>
    using namespace std;
    
    void main()
    {
    	int x = 1;
    	while(x <= 10)
    	{
    		cout << x;
    		x++;
    	}
    }
    

    Mit diesem Code passiert nämlich dasselbe:

    #include <iostream>
    using namespace std;
    
    void main()
    {
    	int x = 1;
    	while(x <= 10)
    	{
    		cout << x;
    		++x;
    	}
    }
    


  • *gähn*

    Interessant wirds doch erst wenn Du cout << ++x oder cout << x++ machst 😃

    Um etwas näher auf die Frage einzugehen: Die Inkrementierung geschieht selbstverständlich in genau der Zeile in der das Inkrement steht. Unterschiede merkst Du erst wenn du den Rückgabewert des Inkrements auswertest.



  • x++ erhöht x um 1, liefert aber den alten x-Wert zurück,
    ++x erhöht x um 1, liefert aber den neuen (um 1 erhöhten) Wert zurück.

    Wenn Du den Operator in einer Zeile (ohne Zuweisung) verwendest, dann ist es völlig egal, welchen Du verwendest.

    Anders sieht es bei Gebilden, wie

    int x = 3;
    x += ++x + x + x++;
    

    beispielsweise aus.

    Während man bei Java das ganze noch aufschlüsseln kann, weil streng von links nach rechts ausgewertet wird, ist das Verhalten afaik in C++ undefiniert, da es im Standard keine definierte Auswertungsreihenfolge gibt.
    Gleiches ist bei Funktionsargumenten zu beachten: foo(x++, --x) oder ähnlichem.

    Solche Konstrukte sollte man allerdings sowieso vermeiden.

    Falls ich mich in irgendeinem Punkt geirrt habe, dann bitte ich um Korrektur. 😃



  • mantiz schrieb:

    Anders sieht es bei Gebilden, wie

    int x = 3;
    x += ++x + x + x++;
    

    beispielsweise aus.

    IMHO ist die Auswertungsreihenfolge durchaus definiert, nämlich von links nach rechts. Das undefinierte
    daran ist, dass mehrmals innerhalb eines Ausdruckes lesend auf 'x' zugegriffen wird.

    mantiz schrieb:

    Gleiches ist bei Funktionsargumenten zu beachten: foo(x++, --x) oder ähnlichem.

    s.o.
    Wobei hier nicht definiert ist, ob erst x++ oder erst --x ausgeführt wird.


  • |  Mod

    C Newbie schrieb:

    mantiz schrieb:

    Anders sieht es bei Gebilden, wie

    int x = 3;
    x += ++x + x + x++;
    

    beispielsweise aus.

    IMHO ist die Auswertungsreihenfolge durchaus definiert, nämlich von links nach rechts. Das undefinierte
    daran ist, dass mehrmals innerhalb eines Ausdruckes lesend auf 'x' zugegriffen wird.

    auf x darf beliebig oft lesend zugegriffen werden, solange dieser lesevorgang dazu dient, den künftigen wert von x zu bestimmen; ausserdem darf x nur einmal zwischen zwei sequencepoints verändert werden - beide regeln sind hier verletzt - ergo UB. die reihenfolge der der auswertung von teilausdrücken, die durch einen operator verknüpft werden ist unbestimmt (diese können sogar überschneiden) ausser es handelt sich um die eingebauten && und || und , bzw. den ?: operator.

    C Newbie schrieb:

    mantiz schrieb:

    Gleiches ist bei Funktionsargumenten zu beachten: foo(x++, --x) oder ähnlichem.

    s.o.
    Wobei hier nicht definiert ist, ob erst x++ oder erst --x ausgeführt wird.

    soweit es sich um die eingebauten inkrement/dekrement-operatoren handelt, ist das UB (sonst wäre es unspezifiziert). das komma zwischen zwei funktionsargumenten ist kein sequencepoint - die reihenfolge der auswertung ist unbestimmt und kann überschneiden.
    beispiel:

    void foo(std::auto_ptr<int>);
    void bar(std::auto_ptr<int>,std::auto_ptr<int>);
    int main()
    {
        foo(std::auto_ptr<int>(new int));
        bar(std::auto_ptr<int>(new int),std::auto_ptr<int>(new int));
    }
    

    der aufruf von foo ist hier völlig ok, es gibt kein speicherleck.
    der aufruf von bar dagegen ist nicht exceptionsicher.



  • Danke, ich habs jetzt wohl verstanden 🙂

    unsigned x = 10;
    
    unsigned wert1 = x++;
    //wert1 ist jetzt 10, da hier x erst nach der Zuweisung inkrementiert wird.
    
    //x ist jetzt 11
    
    unsigned wert2 = ++x;
    //wert2 ist jetzt 12, da hier x vor der Zuweisung inkrementiert wird.
    


  • Wenn der Ausdruck reduziert wird, wird die Postfix-Version
    x++ durch eine Variable ersetzt, der man etwas zuweisen kann; nennen wir sie r1 . Gleichzeitig wird Assemblercode generiert:
    mov [r1],x
    inc [x]
    Bei der Präfix-Version, sieht es hingegen wohl so aus:
    inc [x]
    und dann ist x der R-Wert.
    Was wirklich irritierend ist: der Inhaltsoperator (*x) hat dieselbe
    Assoziativität ( von rechts nach links ) und Priorität wie der Vorzeichen- und Inkrement-/Dekrementoperator. Genauso irritierend ist, daß er aus demselben Token wie der Multiplikationsoperator bestehen muß.
    Da gibt es dann 'interessante' Ausdrücke wie
    ++x= ++*x++ * x;
    Wenn der Compiler seine Tokenbegrenzer auf den Inhaltsoperator schiebt, kann er ihn wohl nur von einer Multiplikation auseinanderhalten, wenn kein Lvalue oder Rvalue im derzeitigen Reduktionsstadium links danebensteht.
    Wenn ein Pointer definiert wird, zum Beispiel
    int *******x;
    werden an sein Token wohl auch spezielle Infos gekettet. Infos wie ob er dereferenziert wird oder wieviele Sterne er noch übrig hat. Ein reduzierter Inhaltsoperator wird wohl durch eine Art zuweisbaren R-Wert mit einem Stern weniger ersetzt.
    Besser kann ich mir da keine Vorstellung machen, wie das geparst wird. Der LR1 prüft halt die Syntax, aber dann übergibt er halt die Schnipsel an andere Routinen mit anderen Algorithmen, die tatsächlich die Übersetzung machen.
    Noch was:
    ++ x =1;
    Das geht nur in C++ , aber nicht in C.
    Und noch was:
    ich vermute, die Sequence-Points hängen historisch damit zusammen, wann auf alten Unix-Geräten mit fflush() die Zeichenstreams geleert wurden.



  • @camper, warum ist foo exceptionsicher und bar nicht?



  • @spiri
    Weil

    das komma zwischen zwei funktionsargumenten ist kein sequencepoint - die reihenfolge der auswertung ist unbestimmt und kann überschneiden.

    Heisst, die beiden new können ausgeführt werden, bevor sie übergeben werden und das zweite ausgeführte new könnte failen.



  • Aha, foo könnte aber genauso failen oder nicht?



  • Äh, nee? Wo ist denn da ein leak, wenn das einzige new failed?



  • Also um das nochmal klarer zu sagen: das Problem ist, das das erzeugte Objekt in bar ggf. noch nicht im smart_ptr gesichert ist, wenn das zweite new failed.



  • Ahja, stimmt.



  • @Jockelx sagte in Unterschied zwischen Postfix und Präfix:

    Also um das nochmal klarer zu sagen: das Problem ist, das das erzeugte Objekt in bar ggf. noch nicht im smart_ptr gesichert ist, wenn das zweite new failed.

    Hm, nein warte. Die Auswertungsreihenfolge ist ja unspezifiziert, aber die Ausdrücke werden trotzdem einem nach dem anderen ausgewertet. D.h. Bevor irgendein zweites new ausgeführt wird, wird der ctor des smart pointers aufgerufen und der Speicher ist somit gesichert.



  • Nee, das Beispiel von Camper gibt es hier (Gotw 56) nachzulesen.
    Sehr gut & genau beschrieben.



  • Am Rande: Ich finde es ja schon etwas gruselig, wenn zwischen zwei Beiträgen eines Threads 13 Jahre liegen...

    @spiri sagte in Unterschied zwischen Postfix und Präfix:

    @camper, warum ist foo exceptionsicher und bar nicht?

    ... besonders wenn man auch noch Rückfragen auf Aussagen erhält, die so lange zurückliegen. Ein Glück dass ich es diesmal nicht bin, der hier von seiner Vergangenheit eingeholt wird 😉

    Finnegan

    P.S.: Geht es hier eigentlich schon um C++03 oder noch um C++98? Damals ging die Unterstützung neuer Standards ja bekanntlich etwas langsamer vonstatten 😉



  • Ach das hab ich tatsächlich nicht mitbekommen. Darum auch auto_ptr 🙂

    Wusste ich jedenfalls noch nicht dass die Evaluierungsreihenfolge überlappen kann.



  • @spiri sagte in Unterschied zwischen Postfix und Präfix:

    Wusste ich jedenfalls noch nicht dass die Evaluierungsreihenfolge überlappen kann.

    Das ist meines erachtens der wichtigste Grund, weshalb man heute zu std::make_unique<int>() rät und selbst für ein unique_ptr<int>(new int) schräge Blicke auf sich zieht (wenn man von dem einsamen intim dynamischen Speicher mal absieht) 😉


  • |  Mod

    Ein Problem mit uralten Threads ist, dass sich die besprochenen Dinge ändern können. Das auto_ptr-Beispiel ist in dieser Form ab C++17 nicht mehr relevant. Die Auswertung der Funktionsargumenten ist untereinander seitdem indeterminately sequenced , ausser dass der Postfixausdruck bei Memberfunktionen vor allen anderen Argumenten ausgewertet wird.
    z.B.

    void f(int x, int y) { cout << x << y << 'n'; }
    int x = 0;
    string s;
    cout << (s+='a') << (s+='b') << '\n'; // gibt "ab" oder "ba" aus
    cout << ++x << ++x << '\n'; // gibt 12 aus
    f(++x,++x) << '\n';  // gibt 34 oder 43 aus
    

    aber:

    int x = 3;
    x += ++x + x + x++;
    

    ist immer noch UB.
    dagegen

    x = 10;
    ++x = 1;
    assert(x==1):
    ++++x;
    assert(x==3):
    

    ist wohlbestimmt.

    *std:unique_ptr<int>(new int{1}) + *std:unique_ptr<int>(new int{1});
    

    ist weiterhin nicht exceptionsicher.