operator= über copy-ctor implementieren?



  • 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...



  • Ich hab das mit den Mengen doch ziemlich gut auf ein Minimalbeispiel reduziert, denke ich. Genau das ist das im Originalcode auch, nur mit allen möglichen Zusatzsachen und Mengen, deren Einträge ich noch erläutern müsste. Aber die Details sind für die Implementierung oder mein Problem alle unwesentlich.

    Also es handelt sich um einen Mengenbaum und die Kind-Elemente sind Untermengen. Die Untermengen mit gleichem Parent sind wie gesagt disjunkt. Wenn der Benutzer etwas einfügen möchte, soll die Methode daher prüfen:

    1. ob die Zahl bereits in einer anderen Menge auf derselben Ebene drin ist
    2. ob die Zahl auch in der Obermenge drin ist, sonst lässt sie sich nicht einfügen

    Edit: Vielleicht sollte man noch dazu sagen, dass diese Mengen über ein UI befüllt werden sollen. Im UI kann man quasi in einer großen Matrix die Zahlen anklicken. Wenn die Zahl nicht in der Obermenge ist, wird die Zahl aber ausgegraut und ein Klick über Menge.setzeZahl(10); führt eben zu nichts. Man kann sich aussuchen, welche Menge man gerade befüllen möchte.

    Gerne liefere ich mehr Details, dafür müsste ich aber auch wissen, was noch als wichtig erachtet wird. 🙂

    Edit: Ich meine, im Prinzip würde ich nur gerne, wenn ich lese "Node copy/moveability ergibt keinen Sinn" hören, wie es denn Sinn ergäbe. Du meintest ja "Tree drüberhauen", dann habe ich gesagt, wieso ich das unschön finde. Und jetzt würde ich wiederum gerne hören, dass meine Argumente in aller Regel für die Tonne sind (und wieso) oder dass das durchaus Sinn ergibt. Dass ich nicht nach einer supertollen Komplettlösung fragen kann, wenn ich die ganzen Detailinfos nicht biete, ist mir klar. Ihr sollt mir da ja auch keine Arbeit abnehmen. Keine Ahnung, ob meine Antwort-Wünsche jetzt total unangemessen sind...



  • krümelkacker hat schon recht, du kannst logischerweise eh immer nur die root-node moven, weil du zum moven Besitz-"Rechte" brauchst, und an die kommste von außen erst mal nicht. Und wenn die Root-Node dann auch ein unique_ptr<node> und keine node ist, dann musst du nur den moven und kannst den ganzen anderen Kram aus der node-Klasse raus holen.

    Kopieren ist allerdings etwas anderes, denn das geht auch mit Teilbäumen. Ist halt die Frage, ob man das überhaupt braucht.



  • Hm, aber wenn ich einen Teilbaum in einen anderen Baum move, dann werden die Kinder eben an den Parent angeknüpft. Genau so wie, wenn ich bei einem echten Waldbaum einen Ast mit Zweigen abreiße und an einen anderen Baum dranklebe (also z.B. einen herauswachsenden Ast durch meinen gemoveten Teilbaum ersetze). Wieso ergibt das keinen Sinn? Man muss halt stets den Parent nachkorrigieren, aber jeder Ast hat über seine Kinder doch das Besitzrecht. Aber vielleicht versteh ich den Einwand nicht ausreichend. Die Untermengen müssen natürlich gemäß der Obermenge nachkorrigiert/reduziert werden.

    Stellt man sich das grafisch dar, kann es doch stets Sinn machen, Teilbäume irgendwo anders reinzumoven (über Drag&Drop). Intern muss halt erst ein neuer Teilbaum angelegt werden, in den kann dann der andere Teilbaum gemoved werden. Oder findet ihr das Quatsch?

    Und die Anwendung einer Teilbaumkopie habe ich bei mir ebenso.

    Okay, dann geht die obige Syntax:

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

    aber natürlich nicht. Wie würdet ihr das denn dann nennen und wie würdet ihr die Schnittstellen basteln, um das umzusetzen?

    So oder anders?

    MengenBaum baum;
    baum.wurzel().setzeZahl(10);
    Menge& untermenge = baum.wurzel().erzeugeKind();
    untermenge.setzeZahl(10);
    
    // Kopie
    Menge untermengenKopie = untermenge;
    
    // Move
    MengenBaum andererBaum = std::move(baum);
    

    Ich fand es nur wie gesagt praktisch, dass ich irgendjemandem eine Menge gebe und die Menge ihre Untermengen dann einfach hat und ich nicht noch eine Baumstruktur drumlegen muss. Findet ihr das nicht praktisch? Oder doch, aber das muss ich dann leider in Kauf nehmen?

    Edit: Hm, und diesen Baum zu serialisieren macht wirklich überhaupt keinen Spaß. Ein index-basierter Ansatz für Kenntnis des Parent und der Neighbors hat mittlerweile so viele Vorteile... man hat halt ne Indirektion mehr, aber die Performance ist hier eigentlich egal. Nur wenn man einem Teilbaum sagt "füge Mal was ein", muss der eben den Parent kriegen, also müsste man ihm den Parent mit übergeben, das ist ja auch Käse.


Anmelden zum Antworten