Imlizites Moven von Returnvalues



  • Eigentlich habe ich gedacht, funktionslokale Variablen, die zurückgegeben werden, werden vom Compiler automatisch gemoved. Offenbar ist das aber doch nicht so.

    #include <iostream>
    #include <utility>
    
    struct foo
    {
    	foo() { std::cout << "foo()\n"; }
    	foo(foo&&) { std::cout << "foo(foo&&)\n"; }
    	foo(foo const&) { std::cout << "foo(foo const&)\n"; }
    	~foo() { std::cout << "~foo\n"; }
    };
    
    foo bar(foo&& f)
    {
    	return std::move(f);
    }
    
    foo baz(foo&& f)
    {
    	return f;
    }
    
    int main()
    {
    	foo f;
    	bar(std::move(f));
    	baz(std::move(f));
    }
    

    Ausgabe:

    foo()
    foo(foo&&)
    ~foo
    foo(foo const&)
    ~foo
    ~foo

    Erwartet hätte ich:

    foo()
    foo(foo&&)
    ~foo
    foo(foo&&)
    ~foo
    ~foo

    Warum ist das so?



  • Könnte völliger Murks sein, den ich schreibe, aber ich meine doch dass RVO so funktioniert, dass ein funktionslokales Objekt, das zurückgegeben wird, direkt in den Stackbereich der aufrufenden Funktion konstruiert wird. Dann wird überhaupt kein Konstruktor aufgerufen. Bei dir wird das Objekt aber nicht in der Funktion konstruiert, daher kann der compiler kein RVO verwenden.


  • Mod

    Rvalue-Referenzen binden nicht an lvalues, weil das ein Sicherheitsproblem darstellt. Folglich wird in

    return a;
    

    der Move-Konstruktor nicht aufgerufen werden. Die Begründung kannst du hier finden.

    Theoretisch wäre hier copy-elision möglich, aber meines Wissens wird das bei der Rückgabe von Funktionparametern von keinem Compiler unterstützt.
    Edit: NRVO geht nat. nicht, weil f kein lokales Objekt, sondern eine Referenz ist.
    Edit2: In C++11 ist NRVO bei Funktionsparametern nicht mehr zulässig.



  • Ich meine mich erinnern zu können, dass es genau für diesen Fall eine Ausnahme gibt. Spricht denn etwas dagegen?



  • Hm, interessant:

    #include <iostream>
    #include <utility>
    
    struct foo
    {
        foo() { std::cout << "foo()\n"; }
        foo(foo&&) { std::cout << "foo(foo&&)\n"; }
        foo(foo const&) { std::cout << "foo(foo const&)\n"; }
        ~foo() { std::cout << "~foo\n"; }
    };
    
    foo bar()
    {
        foo f;
        return std::move(f);
    }
    
    foo baz()
    {
        foo f;
        return f;
    }
    
    int main()
    {
        foo f1 = bar();
        foo f2 = baz();
    }
    

    -->

    foo()
    foo(foo&&)
    ~foo
    foo()
    ~foo
    ~foo
    

    Das explizite benutzen von move() erzeugt also einen unnötigen Konstruktoraufruf. Das hätte ich mir nicht gedacht.

    Warum ist NRVO bei Funktionsparametern nicht zulässig?


  • Mod

    314159265358979 schrieb:

    Das explizite benutzen von move() erzeugt also einen unnötigen Konstruktoraufruf. Das hätte ich mir nicht gedacht.

    Ist schon immer so, auch in C++03 ist

    struct foo { foo& self() { return *this; } };
    [...]
    foo x = foo().self();
    

    kein Fall, in dem RVO angewendet werden kann.

    314159265358979 schrieb:

    Warum ist NRVO bei Funktionsparametern nicht zulässig?

    Keine Ahnung, der Standardtext hat sich geändert. Man müsste mal mit alten Drafts vergleichen, um herauszufinden, wann und warum das geändert wurde.

    C++03 schrieb:

    12.8 Copying class objects [class.copy]
    [...]15 When certain criteria are met, an implementation is allowed to omit the copy construction of a class object, even if the copy constructor and/or destructor for the object have side effects. In such cases, the implementation treats the source and target of the omitted copy operation as simply two different ways of referring to the same object, and the destruction of that object occurs at the later of the times when the two objects would have been destroyed without the optimization.111) This elision of copy operations is permitted in the following circumstances (which may be combined to eliminate multiple copies):
    — in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object with the same cv-unqualified type as the function return type, the copy operation can be omitted by constructing the automatic object directly into the function’s return value
    [...]

    C++11 schrieb:

    12.8 Copying and moving class objects [class.copy]
    [...]
    31 When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the copy/move constructor and/or destructor for the object have side effects. [...] This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):
    — in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cvunqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value
    [...]



  • camper schrieb:

    Ist schon immer so, auch in C++03 ist

    struct foo { foo& self() { return *this; } };
    [...]
    foo x = foo().self();
    

    kein Fall, in dem RVO angewendet werden kann.

    In diesem Fall wird ja eine LValue-Referenz zurückgegeben. In meinem Fall ist es eine lokale Variable per Value, deshalb verstehe ich das nicht so ganz.


  • Mod

    314159265358979 schrieb:

    camper schrieb:

    Ist schon immer so, auch in C++03 ist

    struct foo { foo& self() { return *this; } };
    [...]
    foo x = foo().self();
    

    kein Fall, in dem RVO angewendet werden kann.

    In diesem Fall wird ja eine LValue-Referenz zurückgegeben. In meinem Fall ist es eine lokale Variable per Value, deshalb verstehe ich das nicht so ganz.

    move ist auch bloß eine Funktion, die eine Referenz zurückgibt.



  • Ich meinte den Returnwert der Funktion foo, in deinem Fall self.


  • Mod

    struct foo { foo& self() { return *this; } };
    foo bar() {
        foo x;
        return x.self();
    }
    

    wäre das äquivalente Beispiel für NRVO. Der Grund, weshalb nicht optimiert werden kann ist in beiden Fällen im Prinzip der gleiche: Der initialisierende Ausdruck verweist nur auf das zu kopierende Objekt, hat es aber nicht selbst erzeugt bzw. nennt es nicht ausdrücklich.



  • Ah, okay. Das klingt logisch. Danke 🙂



  • camper schrieb:

    Edit2: In C++11 ist NRVO bei Funktionsparametern nicht mehr zulässig.

    Aber "gemovt" werden darf trotzdem. Nur eben nicht bei Rvalue-Referenzen. Diese Einschränkung ist zwar echt überflüssig, aber es ist leider so.


  • Mod

    krümelkacker schrieb:

    camper schrieb:

    Edit2: In C++11 ist NRVO bei Funktionsparametern nicht mehr zulässig.

    Aber "gemovt" werden darf trotzdem. Nur eben nicht bei Rvalue-Referenzen. Diese Einschränkung ist zwar echt überflüssig, aber es ist leider so.

    Hier der Ursprung der Änderung. Aus meiner Sicht nicht überzeugend die Begründung.
    Die Tatsache, dass nun Rvalue-Referenzen bei return nicht automatisch gemoved werden... wurde womöglich übersehen, was anhand der Entstehungsgeschichte dieses Doppelüberladungsabsatzes (12.8/32) verständlich ist - sich dort mit Referenzen zu beschäftigen, passt systematisch nicht so recht. Ein echter Nachteil ergibt sich dadurch ja nicht, ist der Parameter sowieso schon eine rvalue-Referenz, kann man durch ein explizites move/forward keine Optimierungschancen verpassen.


Anmelden zum Antworten