RAI vs std::unique_ptr



  • @llm

    Hier mal ein vollständiges Beispiel, wie man das mit RAI macht:

    class foo
    {
    public:
        foo() { bla = new bla(); }
        ~foo() { delete bla; }
    
    private:
        bla* bla_instance;
    }
    

    Zurück zu meiner Ausgangsfrage 😉 Wozu std::unique_ptr?



  • foo f1;
    foo f2;
    
    f1 = f2; // Bumm
    


  • @Steffo

    Das ist aber kein RAII - weil du deinen Speicher "von Hand" aufräumst

    Online-Testen: cpp.sh/4w6ds

    #include <iostream>
    #include <memory>
    
    struct test
    {
      test(){ std::cout << "test.ctor" << std::endl; }
      ~test(){ std::cout << "test.dtor" << std::endl; }
    };
    
    struct test2
    {
        test2():t(new test()){}
        std::unique_ptr<test> t;
    };
    
    struct test3
    {
        test3():t(new test()){}
        test* t;
    };
    
    int main()
    {
        test2();
        test3();
    }
    

    Ausgabe:
    test.ctor
    test.dtor
    test.ctor



  • Naja, er macht schon sowas wie RAII. Nur halt manuell und unvollständig. Wenn er das fehlerfrei und vollständig komplettiert hat er aber genau das, was std::unique_ptr macht. Und dann kann man sich natürlich fragen, warum man den nicht sofort benutzt hat.



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

    @Steffo

    Das ist aber kein RAII - weil du deinen Speicher "von Hand" aufräumst

    Online-Testen: cpp.sh/4w6ds

    #include <iostream>
    #include <memory>
    
    struct test
    {
      test(){ std::cout << "test.ctor" << std::endl; }
      ~test(){ std::cout << "test.dtor" << std::endl; }
    };
    
    struct test2
    {
        test2():t(new test()){}
        std::unique_ptr<test> t;
    };
    
    struct test3
    {
        test3():t(new test()){}
        test* t;
    };
    
    int main()
    {
        test2();
        test3();
    }
    

    Ausgabe:
    test.ctor
    test.dtor
    test.ctor

    test3 hat ja auch ein Leak. Bevor es std::unique_ptr gab, ist man doch genau so vorgegangen und nannte das RAII, oder nicht?



  • @Steffo

    Klar - aber der unique_ptr forciert das RAII Konzept für Heap-Dinge



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

    foo f1;
    foo f2;
    
    f1 = f2; // Bumm
    

    Nicht wirklich: http://cpp.sh/8rzfo



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

    Nicht wirklich:

    Doch wirklich. Memory leak und double free.



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

    @Steffo sagte in RAI vs std::unique_ptr:

    Ich sehe da sogar Nachteile, da bei Vorwärtsdeklarationen auch noch eine Funktion für das Delete mitgegeben werden muss,

    Den Satz verstehe ich nicht.

    Das hier funktioniert nicht.

    // 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> bla_instance;
    }
    


  • @Steffo
    In die cpp

    foo::~foo() = default;
    

    und schon geht's.



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



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


Anmelden zum Antworten