wie kann eine riesige Konstruktor-Assignment-Sammlung verhindert werden?



  • Ich habe eine Klasse Foo, die mit einem String, als auch mit der selben Klasse an sich konstruiert werden kann.
    Um nun nicht sinnlose Kopienen anfertigen zu müssen, habe ich mich entschieden, die Move-Semantik auch einzubauen. Allerdings habe ich jetzt 5 Konstruktoren und 4 Assignment-Operatoren. je 2x für String und je 2x für die Klasse selbst.

    Das ist ja extrem viel code und man verliert dabei leicht den überblick.
    Gibt es keine möglichkeit, das anders zu lösen, als so?

    Hier dazu ein "Minimal" working example: (selbst das Minimal-Working example ist extrem riesig.)

    #include <iostream>
    #include <typeinfo>
    #include <typeindex>
    #include <sstream>
    
    class Foo
    {
        public:
        Foo(void):s("default")
        {
            std::cout << "Default Constructor called!" << std::endl;
            printContent();
        }
    
        //copy/move constructors
        Foo(Foo&& f):s{std::move(f.s)}
        {
            std::cout << "Move Constructor called" << std::endl;
            printContent();
        }
    
        Foo(const Foo& f):s{f.s}
        {
            std::cout << "Copy Constructor called" << std::endl;
            printContent();
        }
    
        Foo& operator=(const Foo& f) {
            s=f.s;
            std::cout << "Copy Assignment called" << std::endl;
            printContent();
            return *this;
        }
    
        Foo& operator=(Foo&& f) {
            s=std::move(f.s);
            std::cout << "Move Assignment called" << std::endl;
            printContent();
            return *this;
        }
    
        //string constructors
        Foo(const std::string & param):s{param}
        {
            std::cout << "String Copy-Constructor called!" << std::endl;
            printContent();
        }
    
        Foo(std::string &&param):s{std::move(param)}
        {
            std::cout << "String Move-Constructor called!" << std::endl;
            printContent();
        }
    
        Foo& operator=(const std::string& param) {
            s=param;
            std::cout << "String Copy Assignment called" << std::endl;
            printContent();
            return *this;
        }
    
        Foo& operator=(std::string&& param) {
            s=std::move(param);
            std::cout << "String Move Assignment called" << std::endl;
            printContent();
            return *this;
        }
    
        ~Foo(void) 
        { 
            std::cout << "Destructor called!" << std::endl;   
            printContent();
        }
    
        std::string str()
        {
            return s;
        }
    
        private:
        void printContent(void) {
            std::cout << "  content: '" << s << "'" << std::endl;
        }
    
        std::string s;
    };
    
    int main() {
        auto first=Foo{};
        auto secnd=first;
        auto third=std::move(first);
        auto str_1=static_cast<std::string>("string copy const");
        auto forth=Foo{str_1};
        auto fifth=Foo{"string move const"};
    
        std::cout << std::endl;
    
        secnd=third;
        third=std::move(secnd);
        auto str_2 = static_cast<std::string>("string copy assign");
        forth=str_2;
        fifth="string move assign";
    
        std::cout << std::endl << std::endl << "Content:"  << std::endl;
    
        //this should be avoided, because first is moved
        std::cout << "first:" << first.str() << std::endl;
    
        //this should be avoided, because secnd is moved
        std::cout << "secnd:" << secnd.str() << std::endl;
        std::cout << "third:" << third.str() << std::endl;
        std::cout << "forth:" << forth.str() << std::endl;  
    
        std::cout << std::endl << std::endl << "Destructors:"  << std::endl;
    
        return 0;
    }
    

    Die Ausgabe dazu:

    Default Constructor called!
      content: 'default'
    Copy Constructor called
      content: 'default'
    Move Constructor called
      content: 'default'
    String Copy-Constructor called!
      content: 'string copy const'
    String Move-Constructor called!
      content: 'string move const'
    
    Copy Assignment called
      content: 'default'
    Move Assignment called
      content: 'default'
    String Copy Assignment called
      content: 'string copy assign'
    String Move Assignment called
      content: 'string move assign'
    
    Content:
    first:
    secnd:default
    third:default
    forth:string copy assign
    
    Destructors:
    Destructor called!
      content: 'string move assign'
    Destructor called!
      content: 'string copy assign'
    Destructor called!
      content: 'default'
    Destructor called!
      content: 'default'
    Destructor called!
      content: ''
    

    Mit

    g++ -I. -Wall -pedantic -Wextra -Werror -g -O2 -std=c++14 constructors.cpp
    

    kann das ganze kompiliert werden.

    lg



  • Wenn du auf die unnötigen Debug-Ausgaben verzichtest, hast du mit

    #include <iostream>
    #include <typeinfo>
    #include <typeindex>
    #include <sstream>
    
    class Foo
    {
        public:
        Foo()=default;
        Foo(std::string param):s(std::move(param)) {}
    
        std::string str()
        {
            return s;
        }
    
        private:
        std::string s="default";
    };
    

    genau die gleiche Funktionalität.



  • Ok, danke, aber dabei handelt es sich leider um ein MWE, ich hab leider vergessen, dazuzusagen, dass ich die Klasse benötige, um ein File zu öffnen.

    Das problem dabei ist, dass ich aber auf die gesamten Konstruktoren nicht verzichen will, aber jeder der konstruktoren hat die selbe arbeit, nämlich das file zu öffnen.

    da bleibt mir wohl nichts anderes über, als alle zu deklarieren, oder?

    lg



  • byteboard schrieb:

    Ok, danke, aber dabei handelt es sich leider um ein MWE,

    Leider nicht, denn...

    ich hab leider vergessen, dazuzusagen, dass ich die Klasse benötige, um ein File zu öffnen.

    ... Du hast den relevanten Teil weggelassen.



  • Habe ich bei deiner lösung dann nicht IMMER eine Kopie des Strings?

    Foo(std::string param):s(std::move(param)) {}
    

    Wenn ich beispielsweise

    Foo("r-value-string")
    

    aufrufe, würde unnötigerweise der String neu kopiert oder nicht?

    Mir gehts jetzt nicht sosehr, dass das Stringkopieren so teuer ist,
    das ist wie gesagt ein MWE und die die Datentypen dort sind nur platzhalter.



  • Der Konstruktor öffnet eine Datei!?
    Also ehrlich, das ist doch dann wirklich unsinn da zig Konstruktoren anzulegen, für einen Performance-Gewinn, der nun wirklich in keinem Verhältnis zu dem Rest steht.

    Edit: zu spät.
    Zu deiner neuen Frage:
    Eine Kopie hast du ja eh immmer.
    Du hast beim copy-by-value nur jeweils ein move mehr, als im Optimalfall.



  • Naja ich dachte, dass ich eine kopie eben nicht immer habe

    angenommen ich hab den Code:

    class Foo
    {
        public:
    
    ...
    
        Foo(VeryBigType &&f):s{std::move(f)}
        {
        }
    
    ...
    
        private:
        VeryBigType s;
    }
    

    Wenn ich nun diese Klasse mit dem Code instanziere:

    auto inst=Foo{VeryBigType()};
    

    Dann habe ich eine instanz von VeryBigType \und keine weitere, weil das VeryBigType einfach an den konstruktor weitergereicht wird. => nichts wird zerstört.

    Bei dem Ansatz von defaultree lege ich durch das call by value eine zusätzliche kopie an, die dann dem konstruktor übergeben wird. die ŭrsprüngliche instanz wird zerstört.



  • Call-by-value legt eine Kopie an, ja, aber diese Kopie wird halt bei einem rValue move-constructed.
    Deshalb hast du nur ein move mehr.
    Bei einem lValue hast du ebenfalls nur ein move mehr, da dort ja sowieso kopiert wird, aber die call-by-value version diese Kopie noch moven muss.



  • Cool, danke,
    dann werde ich es wohl mit einem call by value machen.

    lg


  • Mod

    Jockelx schrieb:

    Call-by-value legt eine Kopie an, ja, aber diese Kopie wird halt bei einem rValue move-constructed.

    Für gewöhnlich wird das rvalue ein temporäres Objekt gleichen Typs sein, und dann greift RVO. Es erfolgt also nicht einmal ein Move.


Log in to reply