operator= über copy-ctor implementieren?



  • Eisflamme schrieb:

    Das Doppel-Move ist doch korrekt, oder? Der muss schließlich move-ctor und move-assignment-Operator nutzen, also brauchen wir hier zwei rvalue-Referenzen, somit darf das Ding nicht benannt sein, also zwei Mal move.

    Die Moves sehen mir beide falsch aus. Das innere tut gar nichts, da das Objekt nicht movebar ist. Das Aussere ist unnoetig, da temporaere Objekte direkt gemoved werden koennen.

    Eisflamme schrieb:

    Theoretisch schon, aber ich habe kein swap, daher suche ich eine Lösung, die sich der moveability bereichert. Wie gesagt hat meine Variante jedoch Overhead. Ist der denn größer als beim copy-swap-Idiom?

    Implementier doch einfach swap, sehe das Problem nicht.



  • hustbaer:
    Ist richtig, das ergibt durchaus Sinn und darum werde ich das jetzt auch so machen (hatte nicht an swap gedacht). Hat meine Variante denn bis auf schlechtere Lesbarkeit Nachteile?

    Kellerautomat:
    Wieso ist das innere Objekt denn nicht movebar? Und das äußere brauche ich deswegen, weil mein MSVC sonst eine Doppeldeutigkeit meldet. Das direkte Moven ohne Angabe ist doch wohl eine Optimierung, oder nicht?



  • Eisflamme schrieb:

    Wieso ist das innere Objekt denn nicht movebar?

    Weil es const ist.

    Eisflamme schrieb:

    Und das äußere brauche ich deswegen, weil mein MSVC sonst eine Doppeldeutigkeit meldet. Das direkte Moven ohne Angabe ist doch wohl eine Optimierung, oder nicht?

    Glaube ich dir nicht. Es ist eine Temporary und damit ein RValue. Keine Optimierung.



  • Aber Moment, beim swap muss ich doch wieder alles schreiben, was ich auch im copy-ctor mache, also löse ich damit mein Problem (keine Lust deep copy zwei Mal zu schreiben) doch eigentlich gar nicht.

    Das mit const stimmt natürlich, albern. Aber ohne const& habe ich die Doppeldeutigkeit. Also geht das so wohl nicht.



  • In swap swappst du alle Member. Was machst du denn im Copy Ctor?



  • Mach Dein Beispiel mal konkreter und weniger künstlich. Bitte auch vollständig kompilierbar, wenn's geht...



  • Die Klasse ist ein Baum und die Nodes speichern den Parent, den muss ich dann korrigieren, ist halt keine triviale Kopie.



  • Was hat die Klasse für Datenelemente?



  • Bis auf ein paar native Typen eine struct-Instanz (die kann Copy und Move aber) und einen vector auf eine wiederum andere Klasse, die wiederum nur einfach kopierbare Elemente (zwei ints und eine Strukturinstanz, die nur ints hat) enthält.

    Also wenn ich nicht die Parent- und Neighborzeiger der struct-Instanz (bzw. eines Datenmembers davon) ändern müsste, dann wäre swap schnell gemacht. Genau so wie copy-ctor.


  • Mod

    Eisflamme schrieb:

    Aber Moment, beim swap muss ich doch wieder alles schreiben, was ich auch im copy-ctor mache, also löse ich damit mein Problem (keine Lust deep copy zwei Mal zu schreiben) doch eigentlich gar nicht.

    Ein Trugschluss. Da ist kein Duplikation drin. swap macht schließlich auch etwas mit dem alten Inhalt.



  • Stimmt. Aber das klappt nur, weil die zeigerhaltenden Elemente der Klasse moveable sind, oder? Sonst würde swap ja nicht move sondern copy aufrufen und schon wären die Zeiger wieder schrott.



  • Eisflamme schrieb:

    [...]

    Dann klinke ich mich hier mal aus. Das ist mir zu schwammig.



  • Man kann mein Problem eigentlich ziemlich stark vereinfachen, nämlich auf einen Baum, deren Kinder den Parent und Gruppenmitglieder kennen, die in einer bestimmten Beziehung zum Node stehen.

    class Node
    {
    public:
        typedef std::unique_ptr<Node> NodePtr;
        typedef std::vector<NodePtr> NodePtrs;
        typedef std::vector<const Node*> ConstNodePtrs;
        // ...
    
    private:
        NodePtrs children;
        ConstNodePtr parent; // natürlich ohne Besitz, ist jetzt korrigiert
        ConstNodePtrs specialNeighbors;
    };
    

    Das Ding soll moveable und copyable sein, wie löst man das sauber? Habe gerade Mal ins std::set reingeschaut, aber das ist mir mit den ganzen Basisklassen, Proxyobjekten und Allokatoren gerade etwas unübersichtlich geworden.

    Hm, da fällt mir auf, dass mein Problem doch nicht gelöst ist. Wenn ich move oder swappe, muss der parent-Ptr ja doch wieder angepasst werden. Dafür auch nur der, aber diese Anpassung habe ich trotzdem wieder doppelt (wie gesagt in OP, auslagerbar ist die).

    Gut, dann wäre meine aktuelle Lösung so was:

    Node::Node(const Node&& other) : parent(std::move(other.parent)), children(std::move(other.children)), specialNeighbors(std::move(other.specialNeighbors))
    {
        // foreach children: parent = this setzen
    }
    
    Node& Node::operator=(Node&& other) 
    {
        parent = std::move(other.parent);
        children = std::move(other.children);
        specialNeighbors = std::move(other.specialNeighbors);
    
        // foreach children: parent = this setzen
    }
    
    Node::Node(const Node& other) : parent(other.parent), children(other.children), specialNeighbors(other.specialNeighbors)
    {
        // deep copy, das ist einiger Aufwand
    }
    
    void Node::swap(Node& other)
    {
        std::swap(parent, other.parent);
        std::swap(children, other.children);
        std::swap(specialNeighbors, other.specialNeighbors);
    
        // wieder Anpassung des Parent von children
    }
    
    Node& Node::operator=(const Node& other)
    {
        swap(other); // gut, hier Mal verkürzt
        return *this;
    }
    

    Ungetestet, weil hier im Forum getippt. Habt ihr Verbesserungsvorschläge?



  • Erstmal zu swap: Das lustige an copy-and-swap ist, dass es quasi automatisch auch zu einem move-and-swap geworden ist. Und ich halte das auch für etwas, bei dem man den Wortlaut im Standard vielleicht noch einmal überdenken sollte. Denn nach dem ist

    T& operator = (const T&); // Copy Assignment
    T& operator = (T&&); // Move Assignment
    

    Hm, wie konnte man solche Überladungen noch mal zusammen legen? Ah, genau!

    T& operator = (T); // Copy Assignment + Move Assignment
    

    Für den Standard ist das allerdings nur ein copy assignment operator, was etwas lustig ist. Praktisch gesehen sollte das aber eigentlich keinen Unterschied machen.

    Dann zu deinem Problem: Müssen die Nodes wirklich ihren Parent kennen? Ich frage weil ich diese Struktur schon oft an Stellen gesehen habe, an denen das nicht nötig war, und einfach nur alles verkompliziert hat.



  • Edit:
    Okay, also swap habe ich jetzt richtig kapiert, also gibt es wirklich den doppel-wirksamen operator=, der mit swap funktioniert. Wunderschön! 🙂

    Bzgl. Parent- und Neighbor-Kenntnis. Ich kann das jetzt nur wieder anhand einem hoffentlich geeigneten Parallelbeispiel ausdrücken. Ein Node ist bei mir so etwas wie eine Menge und ein Child ist eine Untermenge. Die Untermengen sind disjunkt. Grundsätzlich kann aber die Menge einfach alle - sagen wir Mal - natürichen Zahlen enthalten.

    Dann könnte man es weiter beschreiben mit: node->SetzeZahl(10); und das soll nur funktionieren, wenn:
    - 10 nicht in der Obermenge (Parent) ist
    - 10 nicht bereits in einer disjunkten Menge (SpecialNeighbors) ist



  • Aber so wie das da aussieht, besitzt eine Node ihr Parent und ihre Children. Macht das wirklich Sinn?



  • Oh, nein, sorry. Mein Beispiel ist falsch...

    Besitz ist natürlich nur von den children. Hab's oben korrigiert.



  • Kopier- und Move-Semantik für so eine Node-Klasse ergibt für mich gar keinen Sinn. Das ist eher die Sache der übergeordneten Tree-Klasse.



  • Hm, das ist bei mir jetzt halt ein Abwasch. Um das Beispiel von den Mengen weiterzuverwenden ist es eben so, dass der Top-Node auch markierte Zahlen hat. Und wenn ich in der Tree-Klasse so etwas wie Root-Node mache, muss ich nahezu alle Methoden daran weiterleiten. Oder man muss sich als Anwender stets den Root-Node holen und die Operationen darauf ausführen (der Baum kam erst später mit rein). Das finde ich auch nicht sonderlich schön.

    Also mit dem Beispiel der Mengen, ich will ja einfach schreiben:

    Menge menge;
    menge.setzeZahl(10);
    menge.setzeZahl(20);
    
    Menge& untermenge = menge.erzeugeKind();
    untermenge.setzeZahl(15); // führt zu nichts
    untermenge.setzeZahl(10); // gut
    

    So finde ich das von der Bedienung her intuitiv. Oder sollte ich das nicht wollen? Wie würdest Du das denn dann umsetzen?



  • Eisflamme schrieb:

    Wie würdest Du das denn dann umsetzen?

    Was denn genau? Entweder bin ich blind, oder du hast immer noch nicht verraten, was das werden soll...


Anmelden zum Antworten