RAI vs std::unique_ptr



  • @Steffo sagte in RAI vs std::unique_ptr:

    @manni66 sagte in RAI vs std::unique_ptr:

    @Steffo
    In die cpp

    foo::~foo() = default;
    

    und schon geht's.

    Ich kann dir wirklich nicht folgen. Es kompiliert und der Konstruktor wird zwei mal aufgerufen:

    http://cpp.sh/8rzfo

    was hat das mit dem zitierten Code zu tun?

    @manni66 sagte in RAI vs std::unique_ptr:

    Doch wirklich. Memory leak und double free.

    Wo kommt hier "Konstruktor wird nicht aufgerufen" vor?

    Siehe auch http://cpp.sh/7hjid



  • @Steffo: Sieh dir mal die Ausgabe von tan: dein Code mit erweiterter Ausgabe

    Vllt. verstehst du jetzt, daß du so auch den Zuweisungsoperator = manuell erstellen mußt?



  • @Th69
    Mit std::unique_ptr habe ich das Problem nicht?
    Ich werde mir das mal nach der Arbeit genauer anschauen. Danke vorab erst mal. 🙂



  • @Steffo
    Mit unique_ptr hast du ein anderes Problem: Er ist nicht kopierbar, d.h. du musst die Kopierkonstuktoren und Zuweisungsoperatoren implementieren. Wobei.... die move-Semantik funktioniert dank unique_ptr automatisch, die Kopiersemantik aber nicht.



  • @DocShoe sagte in RAI vs std::unique_ptr:

    Er ist nicht kopierbar,

    Was ja auch gewünscht ist. Will man kopieren können, muss man genau die Funnktionen schreiben, die man auch im Rawpointer-Fall benötigt hätte. Schreibt man die Funktionen nicht, haut einem der Compiler auf die Finger. Im Rawpointer-Fall hat man UB.



  • Ja, den Wunsch drückt man ausdrücklich durch die Verwendung von std::unique_ptr aus. Steffos Variante und die std::unique_ptr Variante verhalten sich aber unterschiedlich, und Steffo hat nicht gesagt, dass er Kopien verhindern will. Also gehe ich davon aus, dass Kopien möglich sein sollen und das geht mit´m std::unique_ptr halt so nicht.


  • Mod

    Es gibt noch eine ganze weitere Dimension bei der Geschichte: Mehrere Ressourcen. Oder allgemein andere Klassenmember, die bei Initialisierung werfen können. Hat man nämlich einen rohen Zeiger und einer der anderen Member wirft bei Initialisierung etwas, dann hat man ein echt grässliches Problem: Ist dem Zeiger nun eine Ressource zugewiesen worden, oder nicht? Der Destruktor will einfach deleten, aber das geht schief, wenn die Ressource nie belegt wurde (Exceptions im Destruktor sind dein Tod!).

    Jetzt gibt es zwar die berüchtigten function-level-try-blocks:

    # Für die, die das nicht kennen (Braucht man auch nicht wissen!)
    MyClass() try: ptr1(Resource(), may_throw() {} catch (...) {delete ptr1; throw;}
    

    Aber was machst du, wenn Resource() etwas wirft? Man brächte hier echt viel schrecklichen Code, um alles abzudecken. Und ich meine sogar, für zwei oder mehr Ressourcen geht das prinzipiell überhaupt gar nicht, alle Fälle abzudecken.

    Wenn man dedizidierte Handlerklassen benutzt, die jeweils nur genau eine Ressource verwalten (und daher intern dieses Problem nicht haben können), dann bekommt man die perfekte Lösung gratis geschenkt, denn der Destruktor ist dann der Standarddestruktor und der weiß ganz genau, welche Member schon initialisiert waren, als die Exception flog, und kann genau die richtigen wieder abräumen.Damit kann man dann auch viele verschiedene Ressourcen belegen, solange man dies indirekt über Handler tut.

    (Und daher kennt auch niemand function-level-try-blocks, denn die braucht man nur, wenn der Code sowieso scheiße ist, aber wer schlechten Code schreibt, der kennt auch keine function-level-try-blocks)



  • Dieser Beitrag wurde gelöscht!


  • @SeppJ sagte in RAI vs std::unique_ptr:

    Es gibt noch eine ganze weitere Dimension bei der Geschichte: Mehrere Ressourcen. Oder allgemein andere Klassenmember, die bei Initialisierung werfen können. Hat man nämlich einen rohen Zeiger und einer der anderen Member wirft bei Initialisierung etwas, dann hat man ein echt grässliches Problem: Ist dem Zeiger nun eine Ressource zugewiesen worden, oder nicht?

    Das Problem kann man ja umgehen, in dem man die Pointer erst mal mit nullptr initialisiert. Ein delete auf nullptr ist zulässig und es passiert nichts schlimmes.

    @DocShoe hat recht: Ich kann mich mit einem std::unique_ptr unnötig einschränken. Mit rohen Pointern sehe ich da mehr Flexibilität.

    @manni66 Ok, mit einem Default-Destruktor kannst du mit einem std::unique_ptr auf eine Vorwärtsdeklaration machen kann. Diese elegante Lösung hast du aber nicht mehr, wenn du den Destruktor überschreibst. Dann musst du folgendes machen:

    // Vorwärtsdeklaration
    class bla;
    
    class foo
    {
    public:
        foo(); // Instanziierung von Member-Variablen
        ~foo(); // Bei Raw-Pointern wird hier delete gemacht
    
    private:
        std::unique_ptr<bla, MyCustomDeleter> bla_instance;
    }
    

    Zusammengefasst: Ich sehe bei einem std::unique_ptr als Member-Variable nicht unerhebliche Nachteile.



  • @Steffo

    Diese elegante Lösung hast du aber nicht mehr, wenn du den Destruktor überschreibst.

    Was?

    Mit rohen Pointern sehe ich da mehr Flexibilität.

    Nur mehr flexibilität Fehler zu machen.



  • @Steffo
    Du hast in C++ zum Glück nicht mehr die Notwendigkeit, sich so stark um Speicherthemen zu kümmern wie es noch vor 25 Jahren der Fall war. Dies ist ein enormer Fortschritt, wenn ich bedenke wieviel Zeit ich schon damit verbracht habe nach Speicherlecks zu suchen.
    Ein Beispiel

    struct Foo{
       Foo(){
         mem = new int[200];
       }
       ~Foo(){
           if(mem != nullptr){
               delete [] mem; 
          }
       }
       void irgend_eine_methode(){
           delete [] mem;
           mem = new int[200]; // hier ein knall etwas passiert speicher wurde nicht angelegt
       }
       int * mem = nullptr;
    }
    

    In der Methode irgend_eine_mehode muss nicht irgendwas beim new passieren sondern nur etwas das new verhindern. Wenn Foo irgendwann zerstört wird knallt es und schon hast du ein Problem. Bist dann stundenlang auf der Suche und verbrätst wertvolle Lebensenergie.


  • Mod

    @Steffo sagte in RAI vs std::unique_ptr:

    Das Problem kann man ja umgehen, in dem man die Pointer erst mal mit nullptr initialisiert. Ein delete auf nullptr ist zulässig und es passiert nichts schlimmes.

    Toll! Du musst also zukünftig deine Konstruktoren so schreiben:

    MyClass(): ptr(nullptr)
    {
      try
      {
        ptr = Resource();
      }
      catch (...)
      {
        ptr = nullptr;
      }
    }
    

    Und ich bin mir sicher, dass das nicht einmal alle Fälle sauber abdeckt. Und du kannst ptr nicht zur Initialisierung anderer Member nutzen, was man häufig aber muss. Und du musst sicher stellen, dass ptr vor allen anderen Membern initialisert wird. Und wer weiß was ich noch alles vergessen habe. Außerdem das gleiche in allen anderen Konstruktoren, und sauber in Zuweisungen gehandhabt. Und du willst das als offensichtlicher Anfänger machen, wenn ich mit jahrelanger Erfahrung diesen Post schon 3x editiert habe, weil mir immer mehr Sonderfälle einfallen, auf die ich eingehen muss.

    Du hast das oben in deinen Beispielen nicht einmal ansatzweise richtig gemacht, und warst dir des Problems nicht einmal bewusst. Dir wurde als der Hauptvorteil genannt, dass man mit unique_ptr nichts falsch machen kann, und man nicht einmal selber Code schreiben brauchst. Du plädierst gerade ernsthaft für hunderte Zeilen Workarounds um Probleme, derer du dir selber bis vor kurzem nicht einmal bewusst warst.

    Und noch @Bashar : Als Moderator kann ich gelöschte Beiträge sehen. Ich antworte auf deine Frage: 😁



  • Ok, ihr habt mich überzeugt. 😅
    Die Lehre der Geschichte ist: Gerade bei C++ lernt man nie aus und im Ernstfall sollte man sich an die Empfehlungen der Core-Guidelines halten.

    Danke für euer Feedback und eure Geduld. 😇



  • Hier noch ne Präsentation RAII & Scopeguard


Anmelden zum Antworten