Konstruktor Parameter - by Value und Move oder Überladungen



  • Hallo zusammen,

    irgendwie mache ich mir in letzter Zeit immer mal wieder Gedanken darüber, wie ich meine Konstuktoren schreibe.

    Ein inzwischen schon alter Hut ist ja, dass wir inzwischen nicht mehr die Regerl der drei haben, sondern die Regel der fünf.

    Aber häufig gibt es neben den Standard Konstruktoren ja noch andere Konstruktoren, denen man Parameter, zur Initialisierung von Member Variablen, übergibt.

    Bei Stackoverflow habe ich von jemandem gelesen, dass, wenn kopiert werden muss, man auf "call by const reference" verzichten sollte, sondern "by value" aufrufen sollte, und dann in die entsprechende Variable moven.

    Eine andere Möglichkeit wäre ja, jeweils mehrere Überladungen vorzuhalten. Einmal für Const Reference und einmal für Rvalue reference.

    Um das an einem Beispiel zu diskutieren:

    #include <string>
    #include <iostream>
    class Foo {
    public:
      Foo() {
        std::cout << "default constructor foo" << std::endl;
      }
      Foo(const Foo& other): some_member(other.some_member) {
        std::cout << "copy constructor foo" << std::endl;
      }
      Foo(Foo&& other) : some_member(std::move(other.some_member)) {
        std::cout << "move constructor foo" << std::endl;
      }
    
      Foo& operator=(const Foo& other){
        some_member=other.some_member;
        std::cout << "copy assignment foo" << std::endl;
      }
      Foo& operator=(Foo&& other){
        some_member = std::move(other.some_member);
        std::cout << "move assignment foo" << std::endl;
      }
    
      Foo(std::string in) : some_member(in) {}
      std::string some_member="";
    };
    
    class Bar {
    public:
      Bar() = default;
      Bar(const Bar& other) : any_foo(other.any_foo) { std::cout << "copy constructor bar" << std::endl; }
      Bar(Bar&& other) : any_foo(std::move(other.any_foo)) { std::cout << "move constructor bar" << std::endl; }
      Bar& operator=(const Bar&) = default;
      Bar& operator=(Bar&&) = default;
    
      Bar(Foo in) :
        any_foo(std::move(in)) {
        std::cout << "valued constructor bar" << std::endl;
      }
    
      Foo any_foo;
    };
    
    int main() {
     Foo foo;
      Bar bar(std::move(foo));
      std::cout << bar.any_foo.some_member << std::endl;
      std::cin.get();
    }
    

    In dem Beispiel wird der move Konstruktor von Foo 2xmal aufgerufen.

    Wenn ich anstelle des "call by value" Konstruktors, 2 Überladungen nehme

    Bar(Foo&& foo) :
        any_foo(std::move(foo)) {
        std::cout << "valued move constructor bar" << std::endl;
      }
      Bar(const Foo& foo) :
        any_foo(foo) {
        std::cout << "valued copy constructor bar" << std::endl;
      }
    

    Wird der "Move" Konstruktor nur einmal aufgerufen.

    An und für sich, halte ich die Lösung "Call by Value and move" für recht elegant, denn bei mehreren Parametern werden die Überladungsmöglichkeiten die man abdecken muss enorm viel.
    Auf der anderen Seite sehe ich auch immer mal wieder "neuen" Code der das alte Muster einfach mit "const reference" aufruft und dann kopiert.
    Und mein eigenes kleines Beispiel zeigt, dass man eventuell einen "move Konstruktor" mehr hat. Aber das sollte eigentlich keinen merklichen Unterschied machen.

    Übersehe ich einen Punkt, der gegen das Prinzip "Call by Value and Move" spricht? Wie handhabt ihr das?



  • Aus "Effektives modernes C++" von Scott Meyers, Technik 41 Zusammenfassung:

    Bei kopierbaren, günstig zu verschiebenden Parametern, die immer kopiert werden, kann eine Wertübergabe fast so effizient wie eine Referenzübergabe sein. Die Wertübergabe ist leichter zu implementieren und erzeugt weniger Objektcode.

    Das Kopieren von Parametern per Konstruktor kann deutlich teurer sein als das Kopieren per Zuweisung.

    Eine Wertübergabe kann zum Slicing-Problem führen. Daher sollte man sie bei Parametern einer Basisklasse im Allgemeinen nicht einsetzen.

    Das Kapitel ist einigermaßen lang, insofern evtl. mal das Buch besorgen und im Detail nachlesen.



  • Die Signatur eines Kopierkonstruktors hat immer eine Referenz des zu kopierenden Objekts (Quelle C++ Reference). Ich habe mal mit ähnlichen Gedanken gespielt und seltsame Compiler Fehlermeldungen bekommen (auch beim Zuweisungsoperator). Seitdem lasse ich das und versuche da keine Mikrooptimierungen mehr.



  • Schlangenmensch schrieb:

    Übersehe ich einen Punkt, der gegen das Prinzip "Call by Value and Move" spricht?

    Nö. Zumindest nicht nach dem bereits angesprochenen effecitve c++.
    Allerdings nur bei Konstruktoren. Bei 'sink'-funktionen, kann das anders aussehen (wegen buffern).

    Schlangenmensch schrieb:

    Wie handhabt ihr das?

    Anders, da es mich genauso wie DocShoe nicht interessiert und ich mit const& immer gut gefahren bin.



  • DocShoe schrieb:

    Die Signatur eines Kopierkonstruktors hat immer eine Referenz des zu kopierenden Objekts (Quelle C++ Reference). Ich habe mal mit ähnlichen Gedanken gespielt und seltsame Compiler Fehlermeldungen bekommen (auch beim Zuweisungsoperator). Seitdem lasse ich das und versuche da keine Mikrooptimierungen mehr.

    Hier geht es nicht um den Kopierkonstruktor, sondern um Konstruktoren, die die Member initialisieren. Ferner ist die Performance hierbei meiner Einsicht nach nicht von Relevanz, zumal mir noch nie teure Move-Konstruktoren untergekommen sind (lasse mich gern des Besseren belehren). Die Problematik sehe ich eher dort, dass man für n Parameter 2^n Konstruktoren braucht, wenn man alle && - / const& -Kombinationen decken möchte.

    Jockelx schrieb:

    Anders, da es mich genauso wie DocShoe nicht interessiert und ich mit const& immer gut gefahren bin.

    Ihr musstet also wirklich noch nie einen Container in eine Klasse hineinmoven?

    EDIT: Hier noch ein kleines Beispiel, wobei es keine Rolle spielt. Ich vermute, dass für alle STL-Container derselbe Code generiert würde.



  • Fytch schrieb:

    Ihr musstet also wirklich noch nie einen Container in eine Klasse hineinmoven?

    In der Tat extrem selten. Aber hier ging es auch um "irgendwie mache ich mir in letzter Zeit immer mal wieder Gedanken darüber, wie ich meine Konstuktoren schreibe". Also eher, wie Schlangenmensch seine Konstruktoren im Allgemeinen schreiben möchte. Und da bleibe ich beim const&. Gewohnheit halt - schwer abzugewöhnen, wenn es nicht wirklich wichtig ist.



  • @temi: Das Buch werde ich mir zulegen. Schon länger geplant, muss es jetzt mal in die Tat umgesetzt werden.

    Ansonsten, ja, es ging mir primär um den "allgemeinen" Fall. Im Prinzip ein überdenken alter angewohnheiten. Wenn man sagen könnte, Parameter by value zu übergeben (wie Fytch korrekterweise anmerkte, geht es nicht um den Copy Konstruktor) und dann zu moven hat keinen Nachteil im Vergleich zu const& aber unter Umständen, wie in dem Fall mit den Containern, Vorteile, könnte ich mir das angewöhnen.


Log in to reply