[solved] unique_ptr, move und rvalue-Referenzen



  • Hallo,

    ich denke, ich verstehe einige Dinge über die Verwendung von unique_ptr falsch.
    Der operator= erwartet eine rvalue-reference. std::make_unique hingegen gibt einfach einen unique_ptr zurück. Weshalb kann ich hier den Zuweisungsoperator verwenden?

    std::unique_ptr<int> = std::make_unique<int>(42);
    

    Desweiteren: Wieso funktioniert dieser Code?

    #include <memory>
    #include <iostream>
    
    struct foo
    {
        foo(int i) : i(i)
        {
            std::cout << "constructing foo with i = " << i << '\n';
        }
        ~foo()
        {
            std::cout << "destructing foo with i = " << i << '\n';
        }
    
        int i;
    };
    
    std::unique_ptr<foo> create_pointer_1()
    {
        return std::make_unique<foo>(1); // hier wird doch eine kopie erstellt?
    }
    
    std::unique_ptr<foo> create_pointer_2()
    {
        // essentiell dasselbe wie create_pointer_1
        std::unique_ptr<foo> f = std::make_unique<foo>(2);
        return f;
    }
    
    std::unique_ptr<foo> create_pointer_3()
    {
        std::unique_ptr<foo> f = std::make_unique<foo>(3);
        return std::move(f); // der rueckgabetyp von std::move ist doch eine rvalue reference
                             // wie passt das mit dem rueckgabetyp der funktion zusammen?
                             // und wieso ist es voellig egal, ob ich das move hinschreibe oder nicht?
    }
    
    int main()
    {
        std::cout << "-- create_pointer_1() --\n";
        std::unique_ptr<foo> ptr = create_pointer_1();
        std::cout << "-- create_pointer_2() --\n";
        ptr = create_pointer_2();
        std::cout << "-- create_pointer_3() --\n";
        ptr = create_pointer_3();
        std::cout << "-- end of main --\n";
    }
    

    Ausgabe

    -- create_pointer_1() --
    constructing foo with i = 1
    -- create_pointer_2() --
    constructing foo with i = 2
    destructing foo with i = 1
    -- create_pointer_3() --
    constructing foo with i = 3
    destructing foo with i = 2
    -- end of main --
    destructing foo with i = 3
    

    Das Ergebnis ist schön: dreimal wird ein Objekt erstellt, dreimal zerstört. Perfekt. Aber warum?



  • Hyde++ schrieb:

    Der operator= erwartet eine rvalue-reference. std::make_unique hingegen gibt einfach einen unique_ptr zurück. Weshalb kann ich hier den Zuweisungsoperator verwenden?

    std::unique_ptr<int> = std::make_unique<int>(42);
    

    Weil der Rückgabewert einer Funktion die einen Wert zurück gibt (keine Referenz) ein rvalue ist. Ich hoffe das beantwortet die Frage.



  • Hyde++ schrieb:

    der rueckgabetyp von std::move ist doch eine rvalue reference
    wie passt das mit dem rueckgabetyp der funktion zusammen?

    Der Rückgabewert von std::move ist ein rvalue und keine rvalue reference. Daher passt das.

    Hyde++ schrieb:

    und wieso ist es voellig egal, ob ich das move hinschreibe oder nicht?

    return-statements sind ein Sonderfall. Einen Wert der von einer Funktion zurückgegeben wird wird automatisch zum rvalue, daher ist das move hier unnötig.



  • Hm, und wieso wird dann in create_pointer_2 beispielweise nicht ein foo zerstört? Der Pointer wird als Kopie zurückgegeben. Wird hier implizit gemoved? Gibts das?



  • Hyde++ schrieb:

    Wird hier implizit gemoved? Gibts das?

    Siehe Return-Value-Optimisation und Named-Return-Value-Optimisation, der Compiler weiß, dass das lokale Objekt an der schließenden Klammer zerstört werden würde und moved deswegen raus um nicht eine unnötige Copy + ein Delete ausführen zu müssen.

    Wenn du mit Optimierung kompilierst wird es nichtmal rausgemoved - es wird gleich außerhalb der Funktion im Zielspeicherplatz ("in place") erstellt, da wo es hätte hingemoved werden müssen.



  • tkausl schrieb:

    der Compiler weiß, dass das lokale Objekt an der schließenden Klammer zerstört werden würde und moved deswegen raus um nicht eine unnötige Copy + ein Delete ausführen zu müssen.

    Wobei dies nur für return-statements gilt. Das letzte copy für Funktionsparameter in einem Block wird z.B. nicht automatisch zu einem move, auch wenn das Argument direkt danach zerstört wird. Warum eigentlich nicht?



  • Ok, danke für die Antworten. Noch eine Frage:
    Im Wikipedia-Artikel zu RVO steht etwas von der "as-if-Rule". Der Compiler muss also so optimieren, dass sich das Programm so verhält, als wäre nicht optimiert worden (bzw. das Programm muss sich Standardkonferm verhalten). Laut Wikipedia bildet RVO eine Ausnahme dieser Regel. Wenn es diese Ausnahme nicht gäbe, dürfte der von mir gezeigte Code doch nicht kompilieren, oder? Bei der Rückgabe der unique_ptr müsste eigentlich eine Kopie erstellt werden.



  • Hyde++ schrieb:

    Bei der Rückgabe der unique_ptr müsste eigentlich eine Kopie erstellt werden.

    Nö, da wird afaik seit C++11 gemoved.

    TNA schrieb:

    Wobei dies nur für return-statements gilt. Das letzte copy für Funktionsparameter in einem Block wird z.B. nicht automatisch zu einem move, auch wenn das Argument direkt danach zerstört wird. Warum eigentlich nicht?

    Wegen etwaigen Seiteneffekten in Copy/Move ctors und der as-if rule. Finde ich aber auch dämlich, ich wäre dafür, dass man einführt, dass man sich nicht auf die Ausführung solcher Operationen verlassen darf.



  • Hyde++ schrieb:

    Ok, danke für die Antworten. Noch eine Frage:
    Im Wikipedia-Artikel zu RVO steht etwas von der "as-if-Rule". Der Compiler muss also so optimieren, dass sich das Programm so verhält, als wäre nicht optimiert worden

    Im allgemeinen Fall ja, aber bzgl Initialisierung von Objekten mit anderen temporären Objekten desselben Typs darf der Compiler mehr. Er darf diese temporären Objekte wegoptimieren. Das steht so oder so ähnlich im Standard explizit drin und umfasst RVO aber auch andere Situationen. In

    unique_ptr<int> quelle() {
        unique_ptr<int> blah (new int(1729));
        return blah;
    }
    
    int main() {
        auto p = quelle();
    }
    

    gibt es gleich mehrere Stellen, wo was wegoptimiert werden darf. Zum Beispiel darf der Compiler den Rückgabewert von quelle mit blah "verschmelzen", so dass das nur noch ein Objekt ist (NRVO). Er darf auch den Rückgabewert von quelle mit p "verschmelzen" (move elision). Im besten Fall, erzeugt dann die quelle-Funktion das unique_ptr<int>-Objekt direkt an dem Speicherort, welcher hinterher für p benutzt wird. Das ist beobachtbar und wird dementsprechend nicht von der as-if-Regel abgedeckt. Aber es wird trotzdem zugelassen. Es ist eine gute Idee und erspart unnötige Move/Copy-Ctor-Aufrufe.



  • Danke fuer die Antworten.


Anmelden zum Antworten