Smart Pointer Verständnisfrage



  • Ich befasse mich gerade mit Smart Pointer und es kommen sofort einige Fragen auf, da dieses Thema komplett neu für mich ist.
    Angenommen ich habe eine Klasse Test und eine Klasse AnyClass von der ich ein Objekt erzeugen möchte. Im Konstruktor der Klasse Test schreibe ich folgendes:

    Test::Test()
    {
    	std::shared_ptr<AnyClass> sharedPointer(new AnyClass("Test Parameter"));
    }
    

    Wann wird die Instanz von AnyClass gelöscht? Wenn der Destruktor von der Klasse Test aufgerufen wurde? Oder beim Verlassen des Konstruktors? Bei std::unique_ptr weiß ich, dass es gelöscht wird, sobald der Gültigkeitsbereich verlassen wird. Aber bei shared_ptr habe ich noch so meine Schwierigkeiten. 🙄



  • Genauso wie bei unique_ptr. Das '}' endet den Block was den shared_ptr out of Scope laufen lässt, was das AnyClass-Objekt löscht. Der Unterschied ist nur, dass der letzte shared_ptr das Objekt löscht, aber da du eh nur einen hast tritt der Effekt nicht auf.

    Anderes Beispiel:

    void foo(std::shared_ptr<AnyClass> sharedPointer)[
        static std::shared_ptr<AnyClass> sp;
        sp = sharedPointer;
    }
    
    Test::Test()
    {
        std::shared_ptr<AnyClass> sharedPointer(new AnyClass("Test Parameter"));
        foo(sharedPointer);
    }
    

    In diesem Fall überlebt das Objekt von AnyClass, weil es noch einen shared_ptr in foo gespeichert gibt. Wenn du aber den Konstruktor nochmal aufrufst wird der alte sp überschrieben und das Objekt wird gelöscht.





  • Danke für eure Hilfe, doch eine Frage habe ich noch. In meinem Buch steht, dass wenn ich dynamische Objekte erstelle, soll ich immer shared_ptr verwenden.

    Wenn Sie dynamische Objekte erzeugen, verwenden Sie shared_ptr. Über die Zerstörung
    mit delete an einer geeigneten Stelle müssen Sie sich keine Gedanken mehr machen.

    Aber wenn ich innerhalb einer Klasse ein dynamisches privates Objekt erzeugen möchte, was für alle Methoden zugänglich ist und im Destruktor wieder zerstört wird, dann würde ich es im Normalfall so machen wie ich es gelernt habe: Im Konstruktor das Objekt mit new erzeugen und im Destruktor mit delete löschen, so ist es für alle Methoden zugänglich. Hat diese Vorgehensweise irgendwelche Nachteile? Und wenn ich tatsächlich shared_ptr verwenden sollte, wie muss ich es dann machen, dass das Objekt nicht sofort zerstört wird sobald der Konstruktor verlassen wird? Muss ich es irgendwie am Leben erhalten so wie nwp3 es getan hat?



  • spointer schrieb:

    Danke für eure Hilfe, doch eine Frage habe ich noch. In meinem Buch steht, dass wenn ich dynamische Objekte erstelle, soll ich immer shared_ptr verwenden.

    Wenn Sie dynamische Objekte erzeugen, verwenden Sie shared_ptr. Über die Zerstörung
    mit delete an einer geeigneten Stelle müssen Sie sich keine Gedanken mehr machen.

    std::unique_ptr<..> ist die erste Wahl, wenn ein besitzender Zeiger zum Einsatz kommt. std::shared_ptr<..> ist die Ausnahme und nur einzusetzen, wenn es nötig ist (= Besitz nicht klar geregelt).

    spointer schrieb:

    Aber wenn ich innerhalb einer Klasse ein dynamisches privates Objekt erzeugen möchte, was für alle Methoden zugänglich ist und im Destruktor wieder zerstört wird, dann würde ich es im Normalfall so machen wie ich es gelernt habe: Im Konstruktor das Objekt mit new erzeugen und im Destruktor mit delete löschen, so ist es für alle Methoden zugänglich. Hat diese Vorgehensweise irgendwelche Nachteile?

    Nein, dann sollst du std::unique_ptr<..> Verwenden. Mit rohen Zeigern (new/delete) hast du unter Umständen Probleme mit Exceptions. Ausserdem bist du dann gezwungen den Destruktor, den Copy-Konstruktor und den Assignment-Operator zu implementieren.

    spointer schrieb:

    Und wenn ich tatsächlich shared_ptr verwenden sollte, wie muss ich es dann machen, dass das Objekt nicht sofort zerstört wird sobald der Konstruktor verlassen wird? Muss ich es irgendwie am Leben erhalten so wie nwp3 es getan hat?

    Nein. Dann musst du den std::shared_ptr<..> (bzw. std::unique_ptr<..>) zu einem Member machen.

    Edit:
    Bsp.

    #include <memory>
    
    class A
    {
    };
    
    class B
    {
    public:
      B()
        : a_(new A())
      {}
    private:
      // Das Objekt, auf das a_ referenziert, lebt solange
      // wie ein Objekt von B. B besitzt ein Objekt von A.
      std::unique_ptr<A> a_;
    };
    
    int main()
    {
      B b;
    }
    


  • Wobei mit der Benutzung eines unique_ptr als Member muss auch immer der Kopierkonstruktor und der Zuweisungsoperator implementiert werden, weil der unique_ptr sich ja nicht kopieren lässt. Der Regel der großen 3 wird hier aber insofern widersprochen, als dass kein Destruktor nötig ist.



  • Skym0sh0 schrieb:

    Wobei mit der Benutzung eines unique_ptr als Member muss auch immer der Kopierkonstruktor und der Zuweisungsoperator implementiert werden, weil der unique_ptr sich ja nicht kopieren lässt. Der Regel der großen 3 wird hier aber insofern widersprochen, als dass kein Destruktor nötig ist.

    Copy-Ctor und Assignment-Op müssen aber nur dann impl. werden, wenn das besitzende Objekt kopierbar sein soll. Wenn nicht, gibts ein Compilation-Error. Im Gegensatz dazu, wenn man mit rohen besitzenden Zeigern arbeitet. Entweder muss das besitzende Objekt dann noncopyable gemacht werden oder eine sinnvolle Impl. für Kopieren/Zuweisen zur Verfügung gestellt werden.



  • Jop, auf jedenfall weit weit sicherer als rohe Pointer.



  • Skym0sh0 schrieb:

    Wobei mit der Benutzung eines unique_ptr als Member muss auch immer der Kopierkonstruktor und der Zuweisungsoperator implementiert werden, weil der unique_ptr sich ja nicht kopieren lässt. Der Regel der großen 3 wird hier aber insofern widersprochen, als dass kein Destruktor nötig ist.

    Nach meiner Erfahrung ist eher der Normalfall dass man keinen Zuweisungsoperator oder Kopierkonstruktor braucht, weil Objekte die einen unique_ptr enthalten sowieso nicht kopierbar/zuweisbar sein sollen/müssen. Dass der Zuweisungsoperator bzw. Kopierkonstruktor nicht automatisch vom Compiler erzeugt werden können ist dann ein angenehmer Nebeneffekt.

    Ein weiterer Fall den man hin und wieder hat ist dass das Objekt auf das man einen Zeiger hält eh immutable ist (z.B. dynamisch initialisierte Lookup-Tables o.ä.). Wenn man dann einen Kopierkonstruktor und/oder Zuweisungsoperator braucht kann man oft unique_ptr einfach gegen shared_ptr tauschen.



  • Ja das stimmt.
    In dem Fall, an den ich gerade denke, hab ich eine baumartige Datenstruktur gehabt und die internen Zeiger halt als unique_ptr gewählt. Aber die einzelnen Baumknoten mussten ja kopiert werden können, da der Baum kopiert werden konnte.

    Nagel mich nicht auf irgendwelche Designschwächen fest, das ist einige Zeit her 😉



  • spointer schrieb:

    Danke für eure Hilfe, doch eine Frage habe ich noch. In meinem Buch steht, dass wenn ich dynamische Objekte erstelle, soll ich immer shared_ptr verwenden.

    Wenn Sie dynamische Objekte erzeugen, verwenden Sie shared_ptr. Über die Zerstörung
    mit delete an einer geeigneten Stelle müssen Sie sich keine Gedanken mehr machen.

    Aber wenn ich innerhalb einer Klasse ein dynamisches privates Objekt erzeugen möchte, was für alle Methoden zugänglich ist und im Destruktor wieder zerstört wird, dann würde ich es im Normalfall so machen wie ich es gelernt habe: Im Konstruktor das Objekt mit new erzeugen und im Destruktor mit delete löschen, so ist es für alle Methoden zugänglich. Hat diese Vorgehensweise irgendwelche Nachteile? Und wenn ich tatsächlich shared_ptr verwenden sollte, wie muss ich es dann machen, dass das Objekt nicht sofort zerstört wird sobald der Konstruktor verlassen wird? Muss ich es irgendwie am Leben erhalten so wie nwp3 es getan hat?

    Wenn Du im Konstruktor ein Objekt anlegen möchtest und im Destruktor wieder frei geben, warum tust Du das überhaupt dynamisch? Reicht nicht einfach das Objekt als Member der Klasse zu deklarieren?

    Es gibt natürlich Gründe, warum man es dynamisch machen möchte, aber man sollte sich immer mal überlegen, ob es wirklich dynamisch sein muss. Zu oft wird new/delete verwendet.



  • Wenn ich bei der Objekterzeugung Parameter mit übergeben muss, bleibt mir ja keine andere Wahl als es mit Zeigern zu lösen. Ist es nicht besser wenn es dann dynamisch ist, oder bevorzugst du sowas...

    class Test
    {
    public:
    	Test()
    	{
    		AnyClass anyClass("Parameter");
    		anyClassPtr = &anyClass;
    	};
    
    private:
    	AnyClass *anyClassPtr;
    };
    


  • spointer schrieb:

    Aber wenn ich innerhalb einer Klasse ein dynamisches privates Objekt erzeugen möchte,

    Möchtest du das denn wirklich? tntnet hat da vollkommen Recht.

    class Foo
    {
    public:
      Foo() : ptr(new int(1729)) {}
      ~Foo() {delete ptr;}
      Foo(Foo const&);
      Foo& operator=(Foo tmp);
    
    private:
      int* ptr;
    };
    

    ist jedenfalls ein Anti-Pattern. Das geht auch einfacher:

    class Foo
    {
    public:
      Foo() : value(1729) {}
    
    private:
      int value;
    };
    

    und ist natürlich auch nicht auf int , double etc beschränkt.

    Eine Indirektion brauchst du hier erst dann, wenn du Polymorphie haben willst, den dynamischen Typ einfach nicht verraten willst (PIMPL) oder das Objekt optional ist bzw später erst erzeugt werden soll. Und dann würde ich da auch unique_ptr für nehmen, weil man damit am wenigstens falsch macht bzgl Speicherlecks, Doppelfreigaben und versehentliches Teilen.

    Statt unique_ptr könntest du auch shared_ptr einsetzen. Damit wäre auch recht einfach copy-on-write implementiertbar. Man sollte sich jedenfalls klar darüber sein, was passiert, wenn man schreibend auf so ein "internes" Objekt zugreift, welches von mehreren Foo-Instanzen geteilt wird.



  • spointer schrieb:

    Wenn ich bei der Objekterzeugung Parameter mit übergeben muss, bleibt mir ja keine andere Wahl als es mit Zeigern zu lösen. Ist es nicht besser wenn es dann dynamisch ist, oder bevorzugst du sowas...

    class Test
    {
    public:
    	Test()
    	{
    		AnyClass anyClass("Parameter");
    		anyClassPtr = &anyClass;
    	};
    
    private:
    	AnyClass *anyClassPtr;
    };
    

    Das ist ja mal vollkommener Quatsch 😮

    Das "anyClass" objekt im Konstruktor wird zerstört, wenn der Konstruktor fertig ist, aber ein Zeiger wird darauf behalten, ergo: Crash (bzw. undef).

    Und wenn eine Klasse keinen Standardkonstruktor hat und dementsprechend Parameter erwartet, dann nutzt man eben Initialisierungslisten. Da gibt es kein Wenn oder Aber, so geht das. Da über Zeiger irgendwas zu maggeln ist das allerletzte, was einem offen steht.



  • Dann nehmen wir doch mal das Beispiel und machen es richtig:

    class Test
    {
    public:
        Test()
          : anyClass("Parameter")
        {
        }
    
    private:
        AnyClass anyClass;
    };
    

    Das funktioniert und kein rum hantieren mehr mit Zeigern, noch nicht mal smarte. Und nebenbei spart man noch ein paar Taktzyklen, da kein dynamischer Speicher reserviert werden muss.



  • Danke! Ich wusste nicht das ich mit dem Elementinitialisierer auch Parameter übergeben kann, dachte immer ich kann damit nur ganz stumpf Attributen Werte zuweisen, dann hat sich das ja erledigt. 😃


Anmelden zum Antworten