RVO, Retrun by Value oder Smart Pointer



  • Nabend!

    Ich finde mich seit Jahren der Java Entwicklung wieder in einem C++ Projekt wieder und frische gerade meine Erkenntnisse wieder ein wenig auf und bin auf etwas gestoßen wo ich mir noch nicht ganz sicher bist, was genau der beste Weg ist.

    Früher ist mir Code in der Art

    vector<string> MyClass::createLargeStringVector();
    

    sofort ins Auge gestoßen, da dort unnötige Kopien erstellt werden.

    Eine Alternative die ich früher häufig gewählt habe war eine der folgenden

    // Variante 1
    smart_ptr<vector<string>> MyClass::createLargeStringVector();
    
    // Variante 2
    void MyClass::createLargeStringVector(vector<string>& vector);
    

    Wenn ich den Stand der Dinge seit C++11 und der modernen Compiler richtig verstehe, muss ich mir über diese Thematik dank RVO gar keine Gedanken mehr machen und kann einfach alles per Value zurückgeben und lasse den Compiler die Arbeit machen (Sonderfälle die schwer zu optimieren sind, ignorieren wir an dieser Stelle mal).

    Das würde die Entwicklung in C++ meiner Meinung wieder ein Stück effizienter und Code lesbarer machen, da ich mir die smart_ptr und ggf. die typedefs sparen kann.

    Wenn ich allerdings getter auf Instanzvariablen zurückgebe, dann würde ich das aber weiterhin wie folgt machen:

    const string& getString() const;
    

    Habe ich das soweit richtig verstanden? Ich laufe ich so direkt in Probleme?



  • Ja. Nein.



  • Was dein vector Beispiel angeht, Stichwort: move semantics



  • Vielen Dank für Eure beiden Antworten!

    Zum Thema move semantics: Wenn ich das richtig verstehe, würde dort der Rückgabewert der Funktion als ein rvalue vom Compiler verstanden werden und dadurch würde dann der move-Konstruktor aufgerufen werden, was im Prinzip das gleiche Ergebnis wie RVO hat - eine Kopie des Objekts wird vermieden.

    Jetzt stellt sich mir aber die Frage, wann wird vom Compiler RVO und wann der move Konstruktor verwendet? Kann ich mir das vorstellen nach dem Schema: Es gibt einen move-Konnstruktor dann wird dieser immer aufgerufen, gibt es diesen nicht, so versucht der Compiler RVO anzuwenden?!



  • KayBe schrieb:

    Jetzt stellt sich mir aber die Frage, wann wird vom Compiler RVO und wann der move Konstruktor verwendet? Kann ich mir das vorstellen nach dem Schema: Es gibt einen move-Konnstruktor dann wird dieser immer aufgerufen, gibt es diesen nicht, so versucht der Compiler RVO anzuwenden?!

    RVO und Move-Semantik sind zwei sehr verschiedene Dinge, die das selbe Problem lösen, aber eben ... sehr unterschiedlich.

    RVO und NRVO haben immer "Vorrang" vor "move": Wenn (N)RVO möglich (bzw. vorgeschrieben) ist, wird (N)RVO gemacht. Dabei wir das Objekt welches den Returnwert darstellt überhaupt nicht kopiert, es wird "in place" direkt dort erstellt wo der Aufrufer es haben möchte. Die Kopie wird also ganz vermieden, daher kann es auch keinen Overhead durch die Kopie geben.

    Allerdings ist (N)RVO nicht überall möglich, und dann kann "move" zum Einsatz kommen.

    Bei Move-Semantik wird das Objekt sehrwohl kopiert, allerdings wird es "move kopiert". Dabei macht sich der Move-Konstruktor des Objekts zu nutze dass das "Originalobjekt" nach dem Kopieren sowieso gleich zerstört wird. Er kann das Originalobjekt also ruhig "kaputt machen", indem er ihm seine Resourcen klaut. Die einzige Einschränkung dabei ist, dass das Originalobjekt noch korrekt zerstört werden kann - da der Dtor ja trotz des "moves" noch aufgerufen wird.

    Bei std::string, std::vector etc. heisst das: Der Move-Ctor klaut den Speicher des Originalobjekts. Dadurch kann die Speicherallokation vermieden werden, und es müssen auch keine Daten kopiert werden. Das erstellen der Kopie beschränkt sich darauf ein paar wenige Variablen (Zeiger, Integer) zu kopieren, und sie im ausgeschlachteten Originalobjekt auf z.B. Null zu setzen, so dass der Dtor beim Zerstören des Originalobjekts weiss dass es nix freizugeben gibt.

    Bei "move" findet die Kopie also schon noch statt. Der Overhead ist aber üblicherweise sehr gering, da man den "move Konstruktor" eben üblicherweise mit sehr wenig Overhead implementieren kann.


Log in to reply