[SOLVED] unique_ptr als Kennzeichung für Objektverantwortung



  • 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