[SOLVED] unique_ptr als Kennzeichung für Objektverantwortung



  • Hallo zusammen,

    ich hatte vor geraumer Zeit in einem anderen Thread über rValue-Referenzen
    schonmal die Frage gestellt, was ihr davon haltet , dass die Objektverantwortung
    durch einen unique_ptr Pointer ausgedrückt wird.

    Leider habe ich keine Antwort darauf erhalten. Und irgendwie werd ich
    die Frage nicht los 🙄

    Also zum Beispiel:

    void setObject(std::unique_ptr<MyObject>&& obj)
    

    hier über nimmt das Objekt, auf dem die Funktion aufgerufen wird, die Verantwortung
    für die Freigabe

    Bei einem klassischen:

    void setObject(MyObject& obj)
    

    verändert sich die Zuständigkeit nicht.

    Nachteil: Etwas mehr Code & eine minimaler Overhead
    Vorteil: Das lästige "Wer ist für das Objekt verantwortlich" wird explizit ausgedrückt

    Wie steht ihr dazu? Sinnvoll oder nicht?

    Gruß,
    XSpille



  • XSpille schrieb:

    Also zum Beispiel:

    void setObject(std::unique_ptr<MyObject>&& obj)
    

    Ein Setter übernimmt eh keine Objektverarntwortung (ist das der Besitz?). Mit einem weniger abstrakten Beispiel würde ich mehr verstehen.



  • Ich hoffe das folgende Beispiel hilft:

    #include <memory>
    
    class MyObject{};
    
    class Foo{
    
    public:
            Foo() : myObject(0){
            }
    
            ~Foo(){
                    delete this->myObject;
            }
    
            void setObject(MyObject* object){
                    this->myObject = object;
            }
    
            MyObject* myObject;
    
    };
    
    class Bar{
    public:
            void setObject(std::unique_ptr<MyObject>&& object){
                    this->myObject = std::move(object);
            }
    
    private:
            std::unique_ptr<MyObject> myObject;
    };
    
    int main(int argc, char** arg){
    
            MyObject* o1 = new MyObject();
            MyObject* o2 = new MyObject();
    
            Foo foo;
            Bar bar;
    
            foo.setObject(o1);
            bar.setObject(std::move(std::unique_ptr<MyObject>(o2)));
            return 0;
    }
    

    Also klassisch würde ich es machen wie bei Foo.
    Hierbei stellt sich mir jedoch immer die Frage, wielange das Objekt benötigt
    wird und ob Foo das löschen übernimmt oder nicht.
    Oben übernimmt es das Löschen, jedoch kann man es anhand der Signatur nicht
    erkennen.

    foo.setObject(o1);
    

    Wer übernimmt das Löschen von o1? Callee oder Caller?
    Kann man nicht erkennen.

    Bei meiner vorgeschlagenen unique-Pointer-Setter-Funktion hingegen sieht man sofort,
    dass der Callee die Objekt-Verantwortung benötigt:

    bar.setObject(std::move(std::unique_ptr<MyObject>(o2)));
    

    Albern oder sinnvoll?

    Gruß,
    XSpille



  • Mir fällt gerade auf, dass man die rValue-Referenz ja auch auf einen
    Zeiger machen kann...
    Ich denke da spricht nichts gegen...

    #include <memory>
    
    class MyObject{};
    
    class Bar{
    public:
            Bar() : myObject(0){
            }
    
            ~Bar(){
                    delete this->myObject; // EDIT3: Das delete im Destruktor sollte man nicht vergessen ^^
            }
    
            void setObject(MyObject*&& object){
                    // this->myObject = std::move(object); EDIT: Hier brauch ich auch kein move mehr
                    this->myObject = object;
            }
    
    private:
            MyObject* myObject;
    };
    
    int main(int argc, char** arg){
            MyObject* o2 = new MyObject();
            Bar bar;
            bar.setObject(std::move(o2));
            return 0;
    }
    

    Damit hat sich meine eigentliche Frage erübrigt, weil ich zu kompliziert
    gedacht habe 🙄

    Danke an alle, die sich meine Frage durchgelesen haben!

    EDIT 2: Ich bin begeistert 🙂



  • Falls Bar eine Anwendungsklasse ist, was der Name vermuten läßt, ist das alles wieder totaler Unfug.
    Deswegen wollte ich nicht über Object, Foo und Bar reden, sondern über einen konkreten Fall.
    Jetzt hast Du Dir mit Bar einen Smartpointer gebaut. 🤡 Helau 🤡



  • volkard schrieb:

    Falls Bar eine Anwendungsklasse ist, was der Name vermuten läßt, ist das alles wieder totaler Unfug.
    Deswegen wollte ich nicht über Object, Foo und Bar reden, sondern über einen konkreten Fall.
    Jetzt hast Du Dir mit Bar einen Smartpointer gebaut. 🤡 Helau 🤡

    Wieso ist es totaler Unfug? Bar hat natürlich noch (viele) andere Funktionen,
    wie z.B. Erstelle mir mit Hilfe von MyObject etwas.
    Ich hab zum Beispiel einen WebServer, der anhand der URL an bestimmte
    Handler verweist. Wenn ich jetzt eine

    void add(Handler*&&);
    

    habe, dann
    weiß ich, dass er die Handler am Ende löscht.
    Okay... Ist kein setter, aber der Hintergrund ist der Gleiche.

    Ist es dann immer noch Unfug?
    🤡 Alaaf 🤡



  • Warum eine RValue-Referenz auf einen Zeiger? Wenn du eine klare Besitzsemantik deklarieren willst, würde ich eher gleich unique_ptr verwenden. Bei rohen Zeigern weiss man nie auf Anhieb, wer sie verwaltet. Und die RValue-Referenz macht die Verwirrung sicher nicht kleiner.



  • Nexus schrieb:

    Warum eine RValue-Referenz auf einen Zeiger? Wenn du eine klare Besitzsemantik deklarieren willst, würde ich eher gleich unique_ptr verwenden. Bei rohen Zeigern weiss man nie auf Anhieb, wer sie verwaltet. Und die RValue-Referenz macht die Verwirrung sicher nicht kleiner.

    bar.setObject(std::move(o2));
    

    Hier sieht man doch eindeutig, oder?

    std::move heißt für mich persönlich immer "dafür bin ich nicht mehr verantwortlich" und "das Objekt ist danach unbrauchbar"...

    bar.setObject(o2);
    

    So gäbe es ja einen Compiler-Fehler...



  • Es könnte aber z.B. (je nach Anwendung) passieren, dass noch irgendwo eine Kopie des Zeigers ist, z.B. in einem Container. Der zeiger "funktioniert" ja weiterhin. Mit unique_ptr passiert das nicht. Außerdem kannst du dir mit unique_ptr die RValue-Referenzen in der Funktionssignatur sparen



  • Ich kann volkard nur zustimmen. Das, was ich hier an Code-Fetzen gesehen habe ist relativ sinnfrei. Move auf raw-pointern? Sinnfrei. Rvalue-Referenz auf raw-Pointer? Sinnfrei. unique_ptr per rvalue-reference entgegen nehmen? Sinnfrei. Wenn Du unique_ptr benutzen willst, benutze unique_ptr; denn...
    - den kann man versehentlich gar nicht kopieren
    - er wird genauso effizient sein, wie ein roher Zeiger (Compiler-Optimierungen angenommen)
    - er kümmert sich um das Löschen

    Beschreibe ein konkretes Problem, was Du lösen willst.



  • krümelkacker schrieb:

    Move auf raw-pointern? Sinnfrei. Rvalue-Referenz auf raw-Pointer? Sinnfrei.

    Ich verdaue erstmal deine ganzen "Sinnfrei"... 🙄

    Ich habe ein Objekt einer Klasse, die verschiedene Aufgaben bearbeitet.

    Dann mache ich etwas wie:

    TaskInformation* info = new TaskInformation(/*...*/);
    processor.setTaskInformation(info);
    processor.processTask();
    

    Woher weiß ich jetzt, wer die TaskInfo löscht?
    Wenn ich meine sinnfreie Rvalue-Referenz auf raw-Pointer verwende, sehe
    sehe ich explizit, dass die Zugehörigkeit an den Processor über geht.

    processor.setTaskInformation(std::move(info)); // move -> Verantwortung wird übergeben & info enthält danach keine 'gültigen' Daten
    

    EDIT: Ich gebe dir recht, dass es aus Sicht des Compilers sinnfrei ist,
    aber für den Entwickler enthält es eine wichtige Information.
    Im Objekt kannst du es auch einem std::unique_ptr zuweisen, jedoch
    scheint mir die Erzeugung eines std::unique_ptr extra für den
    Parameter überflüssig, solange keiner der weiteren Parameter kopiert
    wird und die Zuweisung am Anfang ist. (Oder hab ich eine potentielle
    Exception-Quelle übersehen?)



  • XSpille schrieb:

    Woher weiß ich jetzt, wer die TaskInfo löscht?
    Wenn ich meine sinnfreie Rvalue-Referenz auf raw-Pointer verwende, sehe
    sehe ich explizit, dass die Zugehörigkeit an den Processor über geht.

    Du hast trotzdem noch rohe Zeiger. Die können aus Versehen an einem anderen Ort gespeichert werden, zudem besteht die allgegenwärtige Gefahr von Memory Leaks.

    processor.setTaskInformation(other.GetPointer()); // !
    
    TaskInformation* info = new TaskInformation(/*...*/);
    StorePointer(info); // !
    processor.setTaskInformation(std::move(info));
    

    Abgesehen davon ist T*&& unintuitiv und geht an bekannten Idiomen vorbei. Kurz: Es ist Gefrickel.

    Gibt es einen Grund, wieso du nicht unique_ptr verwenden willst?



  • Nexus schrieb:

    Gibt es einen Grund, wieso du nicht unique_ptr verwenden willst?

    Ich frage mich nur im Moment, ob er einen zusätzlichen Nutzen hat.

    Nexus schrieb:

    processor.setTaskInformation(other.GetPointer()); // !
    

    Das Argument ist gut! Habe ich nicht berücksichtigt! 👍



  • XSpille schrieb:

    TaskInformation* info = new TaskInformation(/*...*/);
    processor.setTaskInformation(info);
    processor.processTask();
    

    Woher weiß ich jetzt, wer die TaskInfo löscht?

    Naja, es ist Dein Design. Du wirst Dir hoffentlich etwas dabei gedacht haben. Wenn es Dir darum geht, die Schnittstellen selbstdokumentierend bzgl Ownership/Transfer zu gestalten, könntest Du Dir folgende Konvention (soweit möglich) aneignen:
    - Übergabe/Rückgabe von unique_ptr bei Ownership-Transfer
    - Übergabe/Rückgabe von rohen Zeigern sonst.

    XSpille schrieb:

    Wenn ich meine sinnfreie Rvalue-Referenz auf raw-Pointer verwende, sehe
    sehe ich explizit, dass die Zugehörigkeit an den Processor über geht.

    Das ist Blödsinn. Dafür nimmste eben unique_ptr. Und da braucht man auch keine Rvalue-Referenzen für die Übergabe. Vergiss am besten, dass es Rvalue-Referenzen gibt. Dann wirst Du sie wenigstens nicht so missbrauchen wie hier gerade.

    Guck mal:

    unique_ptr<foo> quelle();
    void senke(unique_ptr<foo>);
    

    Hier ist sofort klar, dass ein Ownership-Transfer stattfindet. Das erfordert keine Rvalue-Referenzen bei der Rückgabe/Übergabe. Es ist klar, weil ein unique_ptr das Objekt, auf das er zeigt, besitzt. Er kümmert sich darum und gibt es im Dtor ggf frei. Ein roher Zeiger ist nur ein roher Zeiger. Ob Du einen rohen Zeiger kopierst oder movst, macht absolut keinen Unterschied. Dass Du einen rohen Zeiger per Rvalue-Referenz erwartest, schränkt Deine Funktion nur auf Rvalues ein. Das, was in Deinen Augen hier && und std::move zu leisten scheint, kannst Du ebenso gut durch einen Kommentar machen. Viel besser geht es aber mit unique_ptr.

    XSpille schrieb:

    processor.setTaskInformation(std::move(info)); // move -> Verantwortung wird übergeben & info enthält danach keine 'gültigen' Daten
    

    info enthält dann "keine gültigen Daten", wenn Du den Zeiger innerhalb Deiner Funktion explizit auf 0 setzt. Im Hinblick auf die Existenz von unique_ptr ist das aber recht sinnfrei.

    unique_ptr ist tollerer weil
    - gibt ressource automatisch frei
    - kann nicht versehentlich kopiert werden
    - ist dabei nicht weniger effizient als ein roher zeiger

    kk



  • krümelkacker schrieb:

    Naja, es ist Dein Design. Du wirst Dir hoffentlich etwas dabei gedacht haben. Wenn es Dir darum geht, die Schnittstellen selbstdokumentierend bzgl Ownership/Transfer zu gestalten, könntest Du Dir folgende Konvention (soweit möglich) aneignen:
    - Übergabe/Rückgabe von unique_ptr bei Ownership-Transfer
    - Übergabe/Rückgabe von rohen Zeigern sonst.

    Genauso habe ich es mir ursprünglich gedacht, nur dann hab ich angefangen den
    unique_ptr zu hinterfragen 😉

    krümelkacker schrieb:

    Vergiss am besten, dass es Rvalue-Referenzen gibt.

    Das werde ich sicherlich nicht 🤡

    krümelkacker schrieb:

    Dann wirst Du sie wenigstens nicht so missbrauchen wie hier gerade.

    Ich bezeichne es lieber als Hinterfragen von Techniken 🙄

    krümelkacker schrieb:

    unique_ptr<foo> quelle();
    void senke(unique_ptr<foo>);
    

    Genauso habe ich es in meinem Code.

    krümelkacker schrieb:

    XSpille schrieb:

    processor.setTaskInformation(std::move(info)); // move -> Verantwortung wird übergeben & info enthält danach keine 'gültigen' Daten
    

    info enthält dann "keine gültigen Daten", wenn Du den Zeiger innerhalb Deiner Funktion explizit auf 0 setzt. .

    Das wäre bei unique_ptr nicht anders...

    Danke euch allen! Ich habe mich insbesondere durch das Argument von Nexus
    davon überzeugen lassen, dass der unique_ptr eine geeignetere Wahl ist.



  • Also darf XSpille es nicht, weil es der Konvention entgegensteht.

    Nun frage ich mich, ob man die Konvention nicht ändern(erweitern sollte, auch wenn das ein langwieriger Prozess ist und bestimmt 10 Jahre lang dauern wird.

    Ist es so, daß man bei Übergabe einer rvalue-Referenz davon ausgeht, daß das übergebene Objekt danach kaputt ist? Falls ja, würde man damit in der Tat auch sagen, daß der Besitz übergeht.

    Die Code-Fehler betrafen alle andere Sachen, oder?



  • krümelkacker schrieb:

    - er wird genauso effizient sein, wie ein roher Zeiger (Compiler-Optimierungen angenommen)

    Ist das so?



  • XSpille schrieb:

    krümelkacker schrieb:

    Vergiss am besten, dass es Rvalue-Referenzen gibt.

    Das werde ich sicherlich nicht 🤡

    🙂

    Naja, die Erfahrung zeigt, Rvalue-Referenzen und std::move werden missverstanden, missbraucht und falsch angewendet. Das einfachste Mittel dagegen ist natürlich, selbst keine Referenzen zu definieren, die Rvalue-Referenzen sind. Damit schließe ich die Nutzung von std::move auf move-optimierten Typen gar nicht aus. Gerade weil wir Move-Semantik bekommen, sollte man sich überlegen, ob es in einer Situation vielleicht viel besser wäre, Objekte auch mal wieder "per-value" an Funktionen zu übergeben (bzw zurückzugeben). Die nächste Stufe der "Rvalue-Reference-Awareness" wäre dann das Definieren von eigenen Move-Ctors und Move-Assignments. Was ich damit sagen will, ist, dass die Wahrscheinlichkeit eines Missbrauchs relativ hoch ist, falls eine Rvalue-Referenz irgendwo außerhalb von Parametern für move-ctor/assignment oder perfect-forwarding-Funktionen auftaucht.

    std::move auf einem Ausdrück, der sowieso schon ein Rvalue-Ausdruck war oder einer, der sich auf ein funktionslokales Objekt bezieht, was per return zurückgegeben werden soll, kann sogar schädlich sein. Er verhindert ggf copy elision/RVO und kann in Grenzfällen auch zu undefiniertem Verhalten führen. Beispiel:

    vector<int> primzahlen();
    
    int main()
    {
      for (int i : move(primzahlen())) {
        cout << i << '\n';
      }
    }
    

    UB. Das vector-Objekt stirbt dank std::move zu früh.

    volkard schrieb:

    krümelkacker schrieb:

    - er wird genauso effizient sein, wie ein roher Zeiger (Compiler-Optimierungen angenommen)

    Ist das so?

    Ja. Das kann man erwarten. (empty base class optimization + inlining). Auf Maschinencode-Ebene wird es so aussehen, als ob Du rohe Zeiger benutzt und manuell abundzu welche auf 0 gesetzt bzw Resourced freigegeben hättest.



  • volkard schrieb:

    Nun frage ich mich, ob man die Konvention nicht ändern(erweitern sollte, auch wenn das ein langwieriger Prozess ist und bestimmt 10 Jahre lang dauern wird.

    wie gesagt, beim unique_ptr gibt es keine andere interpretation. bei rohen zeigern, müsste man zusätzlich dokumentieren, ob bei der Übergabe eines solchen rohen Zeigers auch der Besitz des "Pointees" übergeben wird. Das geht weder aus der Signatur noch aus dem Parametertyp (dummer raw pointer) hervor.

    Ist es so, daß man bei Übergabe einer rvalue-Referenz davon ausgeht, daß das übergebene Objekt danach kaputt ist?

    Das kommt drauf an, was die Funktion mit dem Objekt anstellt. Es ist ja nur eine olle Referenz, die übergeben wird. Das ist bei Lvalue-Referenzen ja genauso. 🙂

    Wenn Du allerdings den move-ctor eines unbekannten (generischen) Typs benutzt, kannst Du Dich nur darauf verlassen, dass das Quellobjekt noch zerstörbar ist und ggf auch noch zuweisbar (falls das vorher vom Typ her auch schon ging). Du kannst als Klassen-Designer natürlich zusätzliche Garantien anbieten. std::string und unique_ptr dokumentieren das Verhalten des move-ctors genau und den Effekt auf die Quelle. Aber zerstörbar und zuweisbar (sofern op= vorhanden) sollten mindestens drin sein.


Anmelden zum Antworten