Move Semantik und r-Value Referenz



  • Hallo zusammen

    Möchte ich einen string in eine klasse (Konstruktor) übergeben habe ich das bisher immer per Referenz getan:

    MyClass(const std::string& mystring)
     : _mystring(mystring)
    

    Seit c++ 11 hat man die Möglichkeit das per Move semantik und r-value referenz zu tun.

    MyClass(std::string&& mystring)
     : _mystring(std::move(mystring))
    

    Was mich nun wundert:
    Ich nutze den Resharper für c++ und der schlägt mir für das erste Beispiel vor:
    "Pass by value and use std::move [modernize-pass-by-value]"

    und macht dann das hier draus:

    MyClass(std::string mystring)
     : _mystring(std::move(mystring))
    

    Wieso nun nicht per R-value referenz?



  • Ich bin mir grade unsicher, was der aktuelle C++ Standard sagt, aber mit C++ 11 kannst du deine 2. Version nicht mit l-value Referenzen aufrufen.

    Ich verwende bei sowas tatsächlich in der Regel die Variante, die dir der Reshaper vorschlägt. Es soll eh eine Kopie erzeugt werden, also kann ich das direkt als Call-by-value implementieren und dann move ich das in den entsprechenden Member.



  • Hallo Schlangenmensch

    Ja ok wenn eine Kopie benötigt wird kann ich es mit call by value aufrufen

    auto mystring = "Hallo Welt";
    MyClass myclass(myString);
    

    lässt sich bei einer r-value referenz gar nicht kompilieren
    nur so

    MyClass myclass("Hallo Welt");
    

    Der resharper wundert mich etwas. Manchmal sagt er l-value refernz übergeben und manchmal einen callbyvalue und move semantik.

    Habe noch nicht ganz die Logig dahinter verstanden.



  • Wenn man wirklich das letzte Bisschen Performance rausholen will/muss, dann ist es vermutlich* besser zwei Overloads zu machen (Lvalue und Rvalue). Weil man sich im Rvalue Fall dann 1x move spart. Für die meisten Fälle wird aber wohl die "by-value + move" Variante reichen.

    *: Darauf verlassen sollte man sich aber auch nicht, gerade beim Thema move haben sich schon viele alte Hasen total verschätzt und dann sehr über die Profiling-Ergebnisse gewundert.



  • booster schrieb:

    Hallo Schlangenmensch

    Ja ok wenn eine Kopie benötigt wird kann ich es mit call by value aufrufen

    auto mystring = "Hallo Welt";
    MyClass myclass(myString);
    

    lässt sich bei einer r-value referenz gar nicht kompilieren

    mystring ja auch keine r-value Referenz 😉

    So würde es wieder gehen:

    auto mystring = "Hallo Welt";
    MyClass myclass(std::move(mystring));
    

    Da ich keine Software für Refaktoringvorschläge verwende, kann ich da wenig zu sagen. Aber schon Entwicklern fällt es mitunter schwer, Code so zu schreiben, dass der Compiler das beste da raus holen kann und gut zu lesen ist. Wenn jetzt eine dritt Software solche Sachen generisch entdecken und verbessern soll, kann ich mir vorstellen, dass das nicht ganz optimal funktioniert.



  • mystring ja auch keine r-value Referenz 😉

    verstehe die Aussage nicht. Wie keine r-value referenz.
    Meine Aussage war wenn ich die r-value referenz verwende also beispiel 2 aus meinem Anfangspost dann ...

    Also ich sagte nicht dass die mystring ausserhalb der Klasse eine r-value referenz ist.

    Die innerhalb der Klasse aber schon.



  • Der Konstruktor aus deinem Eingangspost erwartet eine r-value Referenz. Wenn ihm keine r-value Referenz übergeben wird, gibt es ein Fehler beim compilieren.

    Die Übergabe von "mystring" an den Konstruktor ist eben kein r-value, folglich gibt es den, von dir beobachteten, Fehler.



  • Achso, jetzt verstehe ich was du mir sagen willst.
    Wir haben wohl das gleiche gemeint . 👍



  • booster schrieb:

    Möchte ich einen string in eine klasse (Konstruktor) übergeben habe ich das bisher immer per Referenz getan:

    MyClass(const std::string& mystring)
     : _mystring(mystring)
    

    Ja, so hat man das früher gemacht. 🙂

    booster schrieb:

    Seit c++ 11 hat man die Möglichkeit das per Move semantik und r-value referenz zu tun.

    MyClass(std::string&& mystring)
     : _mystring(std::move(mystring))
    

    Das ist zwar so möglich, aber unpraktisch, weil dieser Konstruktor nicht mehr alles akzeptiert, nur noch Rvalue-Argumente:

    string x = "hello";
     MyClass o {x}; // Klappt nicht mehr, da x ein Lvalue ist.
    

    Du könntest jetzt den Konstruktor überladen:

    MyClass(string const& s) : _mystring(s) {}
     MyClass(string && s) : _mystring(move(s)) {}
    

    So funktioniert das wieder wie erwartet. Aber sowas ufert schnell mal aus. Diese Art der Überladung explodiert mit der Zahl der Parameter.

    Der nächste Schwierigkeitsgrad, der die Überladungsexplosion umgeht wäre "perfect forwarding". Aber gerade bei Konstruktoren ist das nicht unproblematisch, weil so ein generischer Forwarding Konstruktor dann auch schnell mal für ein Copy oder Move-Konstruktor gehalten werden kann (bzgl Überladungsauflösung). Deswegen zweige ich das erst gar nicht.

    Warum nicht auch einfach, wenn es einfach geht? ...

    booster schrieb:

    Was mich nun wundert:
    Ich nutze den Resharper für c++ und der schlägt mir für das erste Beispiel vor:
    "Pass by value and use std::move [modernize-pass-by-value]"

    und macht dann das hier draus:

    MyClass(std::string mystring)
     : _mystring(std::move(mystring))
    

    Ja, so würde ich das auch schreiben. Im Vergleich zu der Überladung zwischen const-Referenz und rvalue-Referenz von oben ist diese Lösung natürlich kürzer und etwas leserlicher. Sie kostet ggf nur einen extra "Move". Aber von string wissen wir, dass der Move-Konstruktor sehr effizient ist. Wie das mystring-Objekt konstruiert wird, wird hier dem Compiler überlassen. Das kann, muss aber nicht eine Kopie sein. Im besten Fall wird das alles wegoptimiert ("copy/move elision"). Wenn Du schreibst

    string getstring()
    
        MyClass o {getstring()};
    

    wird bei der pass-by-value Lösung heutzutage die getstring-Funktion das String-Objekt direkt dort konstruieren, wo es später einfach zu mystring umbenannt wird. Und dann findet ein Move von mystring zu _mystring statt, der relativ billig ist.

    Ich finde, dass wir uns gerade bei move-optimierten Typen zu mehr pass-by-value trauen sollten. In diesem Fall sind wir an einer eigenen string-"Kopie" interessiert. Warum als nicht pass-by-value? Lass Den Compiler das Problem lösen, wie dieser Parameter-String zu Stande kommt. Da kann viel optimiert werden.

    In anderen Fällen, nämlich wo man den String nur ggf untersuchen will und keine eigene Kopie braucht, könnte heute string_view interessant sein. Perfekte ist kein Ansatz. Es sind immer Abwägungen. Auch string_view hat seine Schwäche: Man bekommt von einem string_view keinen const char* mehr, der auf eine nullterminierte Zeichenkette zeigt.


Log in to reply