Macht RVO meine Übung kaputt?



  • Hey Leute, ich wollte heute mal anfangen, mich mit RValue Referenzen zu beschäftigen und habe dieses kleine Programm geschrieben:

    #include <iostream>
    #include <utility> //für std::move
    #include <vector>
    #include <cstdlib>
    struct A
    {
    	std::vector<int> n;
    
    	A(A&& a)  {  n.swap(a.n); std::cout<<"swaped"<<std::endl; }
    	A(const A&  a) : n(a.n) { std::cout<<"kopiert"<<std::endl; }
    	A() {}
    };
    
    A f() //erstellt irgendein Objekt vom Typ A
    {
    	A a;
    	a.n.push_back(rand());
    	return a;
    }
    
    int main()
    {
    	A b,c;
    
    	A a1(b); //Erwartet: normaler Konstruktor. Passt.
    	A a2(std::move(c)); //Erwartet: Rvalue Construktor: Passt
    	A a3(f()); //Erwartet: Rvalue Konstruktor. Passt nicht.
    }
    

    Ausgabe:

    kopiert
    swaped

    Das Erzeugen von a1 und a2 läuft so, wie ich es mir denke (über die entsprechenden Konstruktoren). Bei a3 hätte ich erwartet, dass es auch über den RValue Konstruktor initialisiert wird, allerdings wird bei mir keiner der beiden Konstruktoren benutzt. Liegt das daran, dass mein Compiler durch RVO alle Konstruktoraufrufe wegoptimiert oder daran, dass ich RValue Referenzen nicht verstanden habe?



  • Schon richtig so, wird RVO sein.
    Guck mal hier: https://ideone.com/79brIG
    Interessant auch https://ideone.com/mdZgPP <- Was irgendwie bitter ist, ist das überhaupt standardkonform?



  • Diese verdammten optimierenden Compiler 😃 . Dann versuche ich jetzt mal, den Compiler auszutricksen. :p



  • Mein Compiler ist übrigens Visual Studio 2012 und Windows 7 64bit... Wäre mal echt interessant zu wissen, ob der zweite Link eine legale Ausgabe erzeugt.


  • Mod

    cooky451 schrieb:

    Interessant auch https://ideone.com/mdZgPP <- Was irgendwie bitter ist, ist das überhaupt standardkonform?

    Ja, das muss so sein. Das Ergebnis des ?:-Operators ist ein lvalue, das zwar auf eine lokales Objekt verweist, es ist aber nicht der Name dieses Objektes, folglich fällt NRVO aus. RVO geht auch nicht, weil es eben kein temporäres Objekt ist. Macht man explizit eins daraus, gewinnt man auch nichts:

    return a.n.back() > 77 ? A(a) : A(b);
    

    Hier greift zwar RVO, um das Move beim return zu vermeiden, aber erst einmal muss ein temporäres Objekt per Kopie erzeugt werden, und NRVO funktioniert nur mit dem return-Statement selbst... Ein explizites std::move sollte mindestens in Erwägung gezogen werden.

    (Teil-)Abhilfe sollte die Verwendung von if bringen, allerdings bestenfalls für einen Zweig (es sei den der Compiler ist extrem schlau). Denn er muss ja, um die Eliminierung des Moves durchführen zu können, bereits bei der Erzeugung der Objekte wissen, welches am Ende nun zurückgegeben werden wird.

    Edit: if bringt wahrscheinlich auch nichts, weil des return-Objekt erzeugt wird, bevor irgendwelche Destruktoren ausgeführt werden, der Compiler müsste also noch berücksichtigen, das die Zerstörung in diesem Fall trivial ist.

    Als Faustregel: NRVO ist (prizipiell) für ein bestimmtes Objekt technisch möglich, wenn sich innerhalb des potentiellen Scopes dieses Objektes kein return Statement findet, dass ein anderes als dieses Objekt zurückgibt. Diese Regel ist hier verletzt. So könnte es gehen:

    A f() //erstellt irgendein Objekt vom Typ A 
    { 
        A a;
        {
            A b;
            a.n.push_back(rand()); 
            b.n.push_back(rand());
            if ( !( a.n.back() > 77 ) )
                return b;
        }
        return a; 
    }
    

    Bei Rückgabe von b greift hier möglicherweise NRVO.



  • camper: Ich hatte hier kein RVO erwartet, sondern ein move. Er kopiert ja. Dass RVO nicht geht ist schon klar, das hab ich ja aktiv verhindert.


  • Mod

    cooky451 schrieb:

    camper: Ich hatte hier kein RVO erwartet, sondern ein move. Er kopiert ja. Dass RVO nicht geht ist schon klar, das hab ich ja aktiv verhindert.

    Die move-Optimierung ist von ihren Voraussetzungen an RVO gekoppelt:
    move wird genau dann in Erwägung gezogen, wenn (N)RVO zulässig wäre bzw. nur deshalb nicht zulässig ist, weil es sich beim betreffenden Objekt um ein Funktionsargument handelt.



  • Ah okay, das war mir nicht bewusst. Gut zu wissen, falls man sich mal wundert warum da kopiert wird.



  • Nur zum Verständnis, es würde schon reichen

    return a.n.back() > 77 ? move(a) : move(b);
    

    zu schreiben damit wieder gemoved wird... richtig?

    Also was schade ist ist bloss dass es nicht automatisch geht, nicht dass wir hier ein echtes Problem vorliegen hätten...?



  • hustbaer schrieb:

    Nur zum Verständnis, es würde schon reichen

    return a.n.back() > 77 ? move(a) : move(b);
    

    zu schreiben damit wieder gemoved wird... richtig?

    Also was schade ist ist bloss dass es nicht automatisch geht, nicht dass wir hier ein echtes Problem vorliegen hätten...?

    Also rein intuitiv würde ich sagen ja und Ideone sagt auch:

    kopiert
    swaped
    swaped

    Was ich mich nun frage:
    Gibt es einen Unterschied, den man wissen müsste, zwischen

    return a.n.back() > 77 ? move(a) : move(b);
    

    und

    return move(a.n.back() > 77 ? a : b);
    

    Also die Ausgabe ist (zumindest bei Ideone - wie ich auch erwartet habe) identisch.

    Gibts da irgendwas zu berücksichtigen?
    Also eigentlich wird es ja einmal (doppelt )vor dem Vergleich und einmal danach umgewandelt.

    Gruß,
    XSpille



  • müsste eigentlich gleich sein

    5.16 Conditional operator
    4 If the second and third operands are glvalues of the same value category and have the same type, the result is of that type and value category ...


Log in to reply