Big Five und Copy-and-Swap in C++11



  • Nexus schrieb:

    Wo würdest du das Argument per Value nehmen? Die Big Five haben eine festgelegte Signatur, die kannst du nicht einfach ändern...

    dumb_array(dumb_array&&) =default;
    dumb_array(dumb_array const&) =default;
    dumb_array& operator=(dumb_array&&) =default;
    dumb_array& operator=(dumb_array other) { return *this = std::move(other); }
    

  • Mod

    dumb_array& operator=(dumb_array&&) =default;
    dumb_array& operator=(dumb_array other) { return *this = std::move(other); }
    

    Das wird dem Compiler nicht gefallen.



  • =default ist ohnehin keine Lösung. Das Problem ist ja gerade, dass die compilergenerierten Funktionen nicht das Richtige tun.



  • Ich kann mir vorstellen, dass es auch Fälle gibt, wo es ein eigenes swap praktischer/einfacher ist als, 1mal den Movekonstruktor und 2mal die Movezuweisung zu bemühen. In den meisten Fällen macht das aber wahrscheinlich kaum einen Unterschied.

    Statt 5 Operationen, reichen oft auch 4 ("unified assignment operator"):

    class indirect_int
    {
        int* ptr;
    public:
        indirect_int(int i=0) : ptr(new int(i)) {}
    
        ~indirect_int() {delete ptr;}
    
        int value() const {return ptr ? *ptr : 0;}
        operator int() const {return value();}
    
        indirect_int(indirect_int const& x) : ptr(new int(x.value())) {}
        indirect_int(indirect_int     && x) : ptr(x.ptr) {x.ptr = 0;}
    
        // "unified" assignment operator (copy+move)
        indirect_int& operator=(indirect_int temp)
        {
           std::swap(ptr,temp.ptr);
           return *this;
        }
    
        // just for efficiency reasons...
        indirect_int& operator=(int i)
        {
            if (!ptr)
                ptr = new int(i);
            else
                *ptr = i;
            return *this;
        }
    };
    

    Dass der Parameter der Zuweisungsfunktion keine Referenz ist, ist absicht. So ensteht temp entweder als Kopie (falls Argument ein Lvalue war) oder über den Movekonstruktor (falls Rvalue), wenn es nicht sogar ganz wegoptimiert wird (copy elision). Man spart sich etwas Tipparbeit auf kosten von einem zusätzlichen Move in einigen Fällen.

    Mal davon abgesehen, dass die Klasse wenig sinnvoll ist, könnte man das so stehen lassen. Viel Redundanz sehe ich da jetzt nicht.



  • könnte man das so stehen lassen

    Ist zwar nicht Thema, aber

    int value() const {return ptr ? *ptr : 0;}
    

    wuerde ich so nicht stehen lassen. Zumindest mittels assert sollte eine Exception geworfen werden.



  • krümelkacker schrieb:

    Viel Redundanz sehe ich da jetzt nicht.

    Ja, in solchen Fällen hält sich der zu duplizierende Code auch in Grenzen.

    Ich hatte kürzlich einen Fall, da war es etwas komplizierter: Eine Klasse hält einen Container und einen Iterator -- dieser zeigt entweder auf ein Element des Containers oder auf dessen end() . Nun müssen natürlich sowohl Move- als auch Kopieroperationen diesen Iterator umhängen, was einige Zeilen Code erfordert.

    Um Codeduplizierung zu vermeiden, habe ich das momentan so gelöst:

    class MyClass
    {
    public:
                 MyClass();
                 MyClass(const MyClass& origin)   { adopt(origin); }
        MyClass& operator=(const MyClass& origin) { adopt(origin); return *this; }
                 MyClass(MyClass&& source)        { adopt(source); }
        MyClass& operator=(MyClass&& source)      { adopt(source); return *this; }
                 ~MyClass();
    
    private:
        template <typename T>
        void adopt(T& source) // T& == MyClass& oder const MyClass&
        {
            std::ptrdiff_t d = std::distance(source.c.begin(), source.itr);
    
            c = moveOrCopy(source.c);
            ... // kopiere/verschiebe andere Member
    
            itr = std::next(c.begin(), d);
        }
    
        Container c;
        Container::iterator itr;
        ...
    };
    
    template <typename T>
    T moveOrCopy(T& ref)
    {
        return std::move(ref);
    }
    
    template <typename T>
    T moveOrCopy(const T& cref)
    {
        return cref;
    }
    

    Ist halt nicht ganz idiomatisch, weil Member in den Konstruktoren nicht direkt initialisiert werden. Funktioniert daher auch nur, wenn Member default-konstruierbar sind... Aber immerhin vermeidet man Codeduplizierung (zumal adopt() beim ursprünglichen Code noch komplizierter ist).



  • knivil schrieb:

    Ist zwar nicht Thema, aber

    int value() const {return ptr ? *ptr : 0;}
    

    wuerde ich so nicht stehen lassen.

    Joa, kann man sich drüber streiten, ob man das Verhalten da definieren soll oder nicht. Die std::string Implementierungen machen das wahrscheinlich ähnlich mit einem Leerstring statt 0 oder so ...

    knivil schrieb:

    Zumindest mittels assert sollte eine Exception geworfen werden.

    Wie sieht das denn dann aus, wenn man per assert eine Ausnahme werfen will?
    assert( bedingung ? true : (throw Ausnahme(),false) );
    😉
    Dann sollte man aber wohl auch kein NDEBUG definieren. 🙂

    Nexus schrieb:

    [...] Nun müssen natürlich sowohl Move- als auch Kopieroperationen diesen Iterator umhängen, was einige Zeilen Code erfordert.

    I see.

    Du erzeugst da mit moveOrCopy etwas Temporäres. Ist das nötig? Kannst du nicht einfach std::move statt moveOrCopy verwenden? Oder wolltest Du dem Leser deutlich machen, dass ggf doch kopiert wird? Aber dann hätte moveOrCopy trotzdem 'ne Referenz zurückgeben können. Vielleicht hätte man "std::move" eher "std::trymove" nennen sollen oder so. 🙂



  • krümelkacker schrieb:

    Vielleicht hätte man "std::move" eher "std::trymove" nennen sollen oder so. 🙂

    std::move moved nichts und kopiert auch nichts. Meiner Meinung nach wäre die korrekte Bezeichnung std::rvalue_cast



  • Wie sieht das denn dann aus, wenn man per assert eine Ausnahme werfen will?
    assert( bedingung ? true : (throw Ausnahme(),false) );

    Nee einfach nur assert:

    assert( condition && "hey, you idiot :)")
    


  • Ausnahmen wirft man mit throw , knivil. Ein assert(false); führt zum Aufruf von abort , wenn man Assertions nicht deaktiviert hat.



  • krümelkacker schrieb:

    Du erzeugst da mit moveOrCopy etwas Temporäres. Ist das nötig? Kannst du nicht einfach std::move statt moveOrCopy verwenden?

    Hm, doch 🙂

    Ich dachte, std::move() würde auf konstanten Objekten nicht funktionieren. Aber anscheinend gibt es eine Const-Rvalue-Referenz zurück, welche dann indirekt vom Kopierkonstruktor angenommen wird... Danke für den Hinweis!

    Fändest du (und andere) meine Vorgehensweise sonst okay? Oder wie würdet ihr sowas umsetzen, ohne den Code an mehreren Stellen zu haben?



  • krümelkacker schrieb:

    Ausnahmen wirft man mit throw , knivil. Ein assert(false); führt zum Aufruf von abort , wenn man Assertions nicht deaktiviert hat.

    Du machst deinem Namen all Ehre.



  • knivil, wenn du schon vom Threadthema abweichst, um im Kontext komplett irrelevante Kleinigkeiten anzumerken (so viel zu "du machst dem Namen alle Ehre"), dann schreib auch was Richtiges. Und wenn nicht, lass dir wenigstens Kritik gefallen.

    Gerade da du in Wirklichkeit ja nur was gesucht hast, um die erste Version deines Posts (sinngemäss "da fehlt ein Move-Assignment-Operator") zu editieren, solltest du nicht noch gross darauf rumreiten.



  • knivil schrieb:

    krümelkacker schrieb:

    Ausnahmen wirft man mit throw , knivil. Ein assert(false); führt zum Aufruf von abort , wenn man Assertions nicht deaktiviert hat.

    Du machst deinem Namen all Ehre.

    Ähm, bitteschön, auf Dein "Zumindest mittels assert sollte eine Exception geworfen werden." sollte schon ein Einspruch kommen.



  • Hat sonst noch jemand eine Idee, wie man die Big Five mit möglichst wenig doppeltem Code umsetzt? Ich fürchte, für C++11 muss sich hier erst noch ein gängiges Idiom etablieren...


Anmelden zum Antworten