Was ist das Tolle an shared_ptr und unique_ptr?



  • Was ist das Tolle an shared_ptr und unique_ptr? In welchen Situationen kann man die einsetzen?



  • c+++++++++11 schrieb:

    In welchen Situationen kann man die einsetzen?

    Wenn man was tolles machen will.



  • c+++++++++11 schrieb:

    Was ist das Tolle an shared_ptr und unique_ptr? In welchen Situationen kann man die einsetzen?

    durch unique_ptr musste nicht mehr ans delete denken. vor allem nicht alle möglichkeiten durchackern, wo evtl irgend eine exception fliegen könnte und die catchen und dein objekt deleten und die exception weiterwerfen.

    shared_ptr brauchste nicht. der ist viel viel zu übereingesetzt. und wenn du ihn mal doch brauchst, weißte auch schon von selber, was er ist.


  • Mod

    volkard schrieb:

    c+++++++++11 schrieb:

    Was ist das Tolle an shared_ptr und unique_ptr? In welchen Situationen kann man die einsetzen?

    durch unique_ptr musste nicht mehr ans delete denken. vor allem nicht alle möglichkeiten durchackern, wo evtl irgend eine exception fliegen könnte und die catchen und dein objekt deleten und die exception weiterwerfen.

    Noch schöner: Es geht nicht nur um delete. Endlich ist es nur ein Einzeiler, mich um Linux-Pipedeskriptoren zu kümmern.



  • hab gestenr mal ein Projekt von mir mit Smartpointern ersetzt. Übersichtlicher wird es dadurch nicht unbedingt

    unique_ptr<Foo> pv;
    

    In der Klassendeklaration und

    pv = make_unique<Foo>)(...);
    

    im Konstruktorrumpf, bzw. seiner Initialisierungsliste ist insgesamt schon was lang im Vergleich zu Asterisk Notation. Dafür spart man sich aber manuelle Speicherverwaltung und rumgemurkse im Destruktor.

    Hab aber auch mal ne Frage: Im Falle dynamischer Speicherallozierung aufem Heap innerhalb einer Klasse habe ich bisher immer Copy Konstruktoren usw. selbst geschrieben um Deep Copy zu erreichen. Wenn ich jetzt shared_ptr habe, die ja nicht kopierbar sind, wie gehe ich dann vor?


  • Mod

    shared_ptr haben aus gutem Grund keine Deep Copy Semantik. Wenn du einen shared_ptr hast und trotzdem eine Deep Copy wünscht, dann ist wahrscheinlich etwas falsch mit deinem Design. Schließlich ist die Ressource doch geteilt, wieso möchtest du sie kopieren? Falls es doch irgendwie sinnvoll sein sollte: Was genau ist das Problem? Das Vorgehen zum Kopieren ist doch, nun ja, einfach kopieren.



  • Ok Sepp aber stell Dir vor, du hast eine Klasse, von der du Instanzen erzeugst. Nun willst du ein genau gleiches Objekt wie ein schon bestehendes erzeugen. Es soll inhaltlich vollkommen identisch sein mit dem ersten Objekt, aber seine eigenen, formal identischen, Ressourcen auf dem Heap verwalten. Wie kann man das mit Smart Pointern lösen?



  • struct Foo
    {
    	int i;
    
    	explicit Foo(int i)
    	 : i(i)
    	{}
    };
    
    int main()
    {
    	auto pfoo = std::make_unique<Foo>(5);
    
    	auto pfoo2 = some_magic(pfoo);
    
    	assert(pfoo == pfoo2);
    	assert(*pfoo == *pfoo2);
    }
    

    Ich denke, du meinst sowas in der Richtung.

    Okay, der einfache Fall ist folgender:

    std::unique_ptr<Foo> some_magic(std::unique_ptr<Foo> const& pf)
    {
    	return std::make_unique<Foo>(*pf);
    }
    

    Du rufst also den Kopierkonstruktor deines Objektes auf.

    Problem:
    Wenn dein Objekt eine Basisklasse ist, wo eine ganze Vererbungshierarchie dranhängt, oder es einfach nicht kopierbar (d.h. keinen Kopierkonstruktor anbietet) ist.

    Da kansnt du mit einer Clone Funktion, die jede Unterklasse implementieren muss arbeiten. Aber das ist recht fehleranfällig.



  • Also nur über diese Umwege mit ner Hilfsfunktion ja?

    Bietet es sich da nicht an, eher auf raw pointer bzw. shared pointer zu gehen?



  • Es kommt immer drauf an, was du vorhast oder was gewünscht ist.

    Grundsätzlich heisst die Devise erstmal, keine Pointer, sondern Objekte.
    Wenn aber nun mehrere Klassen was voneinander wissen sollen bzw. eine Assoziation haben, dann kann man dafür Referenzen nutzen. Wenn die zu strikt sind (Stichwort Kopierbarkeit!) dann kann man auf rohe Zeiger umschwenken.
    Aber, und das sit eigentlichd er ganze Clou dahinter, diese rohen Zeiger sind nicht besitzend, d.h. sie müssen sich niemals um delete oder sowas kümmern.

    Viele denken, dass man direkt im ganzen programm niemals mehr auch nur einen einzigen rohen Zeiger haben darf, sondern alles mit SmartPointern machen muss.

    int main()
    {
    	Observable o = /* some fancy stuff ... */;
    
    	Foo f1;
    	Foo f2;
    	Foo f3;
    
    	o.registerObserver(f1);
    	o.registerObserver(f2);
    	o.registerObserver(f3);
    
    	change(o);
    
    	f1.doShit();
    	f2.doFoo();
    	f3.doBar();
    }
    

    Stell dir den Code vor, was das Oberserver Pattern nutzt. Ein Objekt o wird von mehreren Observern beobachtet, d.h. sie sollen auf Änderungen reagieren.
    Soll der Observer jetzt Kopien speichern? Oder SmartPointer?

    struct Observable
    {
    	std::vector<Foo> observers;
    
    	void notify()
    	{
    		for(auto const& o : observers)
    			o.notify(*this);
    	}
    };
    

    bzw.

    struct Observable
    {
    	std::vector<std::unique_ptr<Foo>> observers;
    
    	void notify()
    	{
    		for(auto const& o : observers)
    			o->notify(*this);
    	}
    };
    

    Da stellt sich dann die Frage wie die registriert werden:

    struct Observable
    {
    	//std::vector<Foo> observers;
    	std::vector<std::unique_ptr<Foo>> observers;
    
    	void notify()
    	{
    		for(auto const& o : observers)
    			o->notify(*this);
    	}
    
    	void registerObserver(Foo /*const*/& f)
    	{
    		//observers.push_back(f);
    		observers.push_back( std::unique_ptr<Foo>(&f) );
    	}
    };
    

    Und das sieht schonmal sehr falsch aus. In dem einen Fall werden nicht die registrierten Observer benachrichtigt, sondern nur Kopein von denen. Und im zweiten Fall werden ggf. Stack-Objekte (deren Besitz und Verantwortlichkeit wo ganz anders liegen) von dieser Klasse gekillt.

    Die Lösung:

    struct Observable
    {
    	std::vector<Foo*> observers;
    
    	void notify()
    	{
    		for(auto const& o : observers)
    			o->notify(*this);
    	}
    
    	void registerObserver(Foo /*const*/& f)
    	{
    		observers.push_back(f);
    	}
    };
    

    Das führt zu einer letzten Überlegung: was machen wir wenn der Zeiger auf tote Objekte zeigt (z.b. weil das Objekt schon seinen Gültigkeitsbereich verlassen hat oder bei Temporaries). Naja, das kann man ganz einfach dem Client überlassen.

    int main()
    {
    	Observable o = /* some fancy stuff ... */;
    
    	Foo f1;
    	Foo f2;
    
    	o.registerObserver(f1);
    	o.registerObserver(f2);
    
    	{
    		Foo f3;
    		o.registerObserver(f3);
    		change(o);
    	}
    
    	change(o); // (!) crash wenn f3 nicht deregistriert wurde
    
    	f1.doShit();
    	f2.doFoo();
    	f3.doBar();
    }
    

    Oder, wenn man das echt will kann man natürlich hier mit SharedPointern arbeiten. Aber das ist dann ein schlechteres unklareres Design, was schwer an Java erinnert.



  • Vielen Dank für die ausführlichen Bemühungen!

    Was für eine Begründung kann es denn geben dafür den Heap zu verwenden, statt dem Stack? Außer dynamischer Speicher Allozierung zur runtime?

    Ich sehe immer wieder Code, in dem Pointer in der private Section der Class Deklaration liegen und dann im Konstruktoraufruf Speicher auf dem Heap diesen Pointern zugewiesen wird. Und das ohne zusätzliche Informationen die erst zur runtime verfügbar wäre.

    Wieso sollte man stattdessen nicht einfach in der private Section direkt eine Objekt Instanz definieren? Weil die Klasse damit dann "größer" wird?



  • Achtung! Nur weil etwas gemacht wird heisst es nicht, dass das gut ist.

    Beispiel:

    class NeuralNetwork
    {
    	MultiLayerPerceptron * mlp;
    	StatisticLayer * stat;
    	ScalingLayer * scale;
    	UnscalingLayer * unscale;
    	// ... mehrere weitere
    
    public:
    	NeuralNetwork(int n)
    	{
    		mlp = new MultiLayerPerceptron(n);
    		stat = NULL;
    		scale = NULL;
    		unscale = NULL;
    	}
    
    	NeuralNetwork(NeuralNetwork const& other)
    	{
    		mlp = new MultiLayerPerceptron(*other.mlp);
    		stat = new StatisticLayer(*other.stat);
    		scale = new ScalingLayer(*other.scale);
    		unscale = new UnscalingLayer(*other.unscale);
    	}
    };
    

    Dieser Code stammt vom Prinzip her aus der OpenNN Bibliothek. Und wie gut der ist, kannste dir selbst an 0.3 Fingern ausrechnen. (Ja, das ist mein Ernst! Mein voller Ernst...)

    Ein sinnvoller Einsatzzweck ist, wenn du das konkrete Objekt nicht kennst. Um beim Beispiel vom Observer-Pattern zu bleiben, der Observable braucht als Interface für die Observer nur ein Observer Interface, die konkreten Klassen/Objekte sind ihm egal (sollte jedenfalls).



  • Sewing schrieb:

    Wieso sollte man stattdessen nicht einfach in der private Section direkt eine Objekt Instanz definieren? Weil die Klasse damit dann "größer" wird?

    Weil es die Compile-Zeiten hochtreibt.
    Weil man damit ne ABI-Dependency schafft.

    Google mal ein bisschen zu den Begriffen "compilation firewall", PIMPL und "abi compatibility c++".

    Gibt also schon manchmal gute Gründe dafür. Kommt halt immer auf die Situation an.



  • Skym0sh0 schrieb:

    Achtung! Nur weil etwas gemacht wird heisst es nicht, dass das gut ist.

    Beispiel:

    class NeuralNetwork
    {
    	MultiLayerPerceptron * mlp;
    	StatisticLayer * stat;
    	ScalingLayer * scale;
    	UnscalingLayer * unscale;
    	// ... mehrere weitere
    	
    public:
    	NeuralNetwork(int n)
    	{
    		mlp = new MultiLayerPerceptron(n);
    		stat = NULL;
    		scale = NULL;
    		unscale = NULL;
    	}
    	
    	NeuralNetwork(NeuralNetwork const& other)
    	{
    		mlp = new MultiLayerPerceptron(*other.mlp);
    		stat = new StatisticLayer(*other.stat);
    		scale = new ScalingLayer(*other.scale);
    		unscale = new UnscalingLayer(*other.unscale);
    	}
    };
    

    Dieser Code stammt vom Prinzip her aus der OpenNN Bibliothek. Und wie gut der ist, kannste dir selbst an 0.3 Fingern ausrechnen. (Ja, das ist mein Ernst! Mein voller Ernst...)

    Ein sinnvoller Einsatzzweck ist, wenn du das konkrete Objekt nicht kennst. Um beim Beispiel vom Observer-Pattern zu bleiben, der Observable braucht als Interface für die Observer nur ein Observer Interface, die konkreten Klassen/Objekte sind ihm egal (sollte jedenfalls).

    was genau stört dich an dem Code?


  • Mod

    Sewing schrieb:

    was genau stört dich an dem Code?

    Das erste, was direkt ins Auge fallen muss, ist, dass hier mehrere Ressourcen von einer einzigen Stelle verwaltet werden. Schreib mal einen Destruktor dafür. Und dann erklär mir, wie du mit dem Fall zurecht kommst, wenn zwischen Zeile 12 und 15 oder zwischen 20 und 23 eine Exception fliegt.



  • Ok das merke ich mir, danke Sepp. Aber wie würde man das denn sonst lösen, wenn man im Construktor mehrere Objekte auf dem Heap anlegen will?


  • Mod

    Wenn das Problem ist, dass man mehrere Ressourcen in einer einzigen Klasse nicht sauber gehalten bekommt (oder nur mit sehr großem Aufwand), dann ist die logische Konsequenz doch, dass man für jede einzelne Ressource einen einzelnen Halter einführt. Lass mal scharf nachdenken: Wie wäre es mit unique_ptr?

    Das heißt nicht, dass das unbedingt die beste Lösung sein muss, um den gezeigten Code zu verbessern. Die Absicht des Programmierers scheint zu sein, eine Klasse mit optionalen Eigenschaften einzuführen. Das ginge sicherlich besser, aber ohne mehr Kontext ist dies nur Spekulation.

    Wer ist eigentlich dieser "Sepp", von dem du dauernd redest? Ich kenne niemandem im Forum mit diesem Nutzernamen.



  • Und wo ich hart drüber gestolpert bin:
    Ja eine Reihe der Member sind optional, per Pointe rumgesetzt, werden aber im Kopierkonstruktor ohne wenn und aber einfach dereferenziert.
    Also im Prinzip der Versuch etwas Optional zu machen, aber dann doch nicht so ganz sauber.


Log in to reply