SmartPointer - von bereits initilisierten Variablen



  • Hey 🙂

    Ich versuche grade von rohen Pointern wegzukommen, und habe mich jetzt ein bisschen um die shared_ptr gekümmert. (Ich hoffe nicht, dass Titel und verwendeter pointer jetzt doch nicht das Selbe ist)

    Wenn ich eine Klasse neu erstelle, macht ein shared_ptr ja wirklich Sinn, und ist sehr praktisch.

    shared_ptr<_Console> pcons(new _Console());
    

    Aber was mache ich mit bereits initialiserten Variablen?
    bzw macht sowas Sinn:

    void 		_Console	::	find_command(shared_ptr<string> str)
    {
    	//doSth
    }
    

    Oder kann/sollte man hier vlt. einfach eine referenz verwenden?
    Es wäre auch call by value möglich, es ging mir nur darum nicht 100 Zeichen zu schicken, sondern nur einen Pointer.

    Insbesondere habe ich grade probleme mit einem kaputten Heap.
    Ich erstelle in einer Funktion 2 Klassen, die shared_ptr speichere ich zusätzlich in der Klasse selbst, und in den erstellten Klassen. Trotzdem bekomme ich beim Schließen des Programms, einen Heap Error.

    BOOL testDlg::OnInitDialog()
    {
    
    	m_pTXTConsole	= shared_ptr<CString>(&m_TXTConsole);
    	m_pCData		= shared_ptr<_CData>(new _CData(m_pTXTConsole));
    	m_pConsole		= shared_ptr<_Console>(new _Console(m_pCData));
    
    	return TRUE;
    }
    


  • Also erstmal sind Klassen das hier:

    class Foo
    {
    // ...
    };
    

    Was du als "neue Klasse erstellen" bezeichnest ist das Erzeugen eines Objektes (oder einer Instanz) einer Klasse:

    int main()
    {
        Foo f; // f ist Instanz/Objekt der Klasse Foo
    }
    

    Dann zu deinem ersten Satz: Der shared_ptr ist ein Smartpointer (ein schlauer Zeiger), da er das, wodrauf er zeigt, selbst löscht und du das nicht manuell machen musst.

    Wie du richtig sagst wird ein shared_ptr so initialisiert:

    std::shared_ptr<Foo> pf(new Foo);
    

    Und der große Vorteil des shared_ptr ist die Kopierbarkeit:

    void bar(std::shared_ptr<Foo> f)
    {}
    
    int main()
    {
        std::shared_ptr<Foo> pf(new Foo);
        std::shared_ptr<Foo> xy(pf);
        std::shared_ptr<Foo> uv = xy;
    
        bar(pf);
    }
    

    Das heisst, du kannst einen Call-By-Value durchaus durchführen. Trotzdem übergibt man den shared_ptr meist als const& Parameter, da dies schneller ist und der Ptr selbst eig nicht kopiert werden braucht.

    Was du trotz allem NIEMALS machen darfst (auch nicht mit rohen Zeigern) ist folgendes:

    int main()
    {
        /* auto */ int i = 0;
        // ...
        int * p = &i;
        delete p;
        // ...
    }
    

    Das Äquivalent mit Smart Pointern:

    int main()
    {
        /*auto*/ int i = 0;
        std::shared_ptr<int> p(&i);
        std::unique_ptr<int> p2(&i); // geht auch hiermit
    }
    

    Das crasht oder ruft mindestens undef Behaviour hervor.

    Denn: mit dem Zeiger löschst du bei dem delete (bzw. beim automatischen Destruktoraufruf der Smart-Ptr) den Speicher des Integers. Aber da dies eine Stackvariable ist (mit Speicherklasse auto) kümmerte sich der Compiler schon darum und versucht danach das i nochmals zu löschen. *aua*



  • void        _Console    ::  find_command(shared_ptr<string> str) 
    { 
        //doSth 
    }
    

    Nein, hier würdest du einfach eine const -Referenz benutzen.
    Smart-Pointer werden bei Funktionsparametern verwendet, wenn die Funktion den Besitz für eine Gewisse Zeit übernimmt. In der Funktion wird dann der Smart-Pointer einfach als Absicherung verwendet, sodass beim Werfen einer Exception das Objekt automatisch zerstört wird.
    Dass bei einer Funktion shared_ptr Sinn macht (anstelle von unique_ptr ) sehe ich auch nicht direkt*.

    * Man könnte argumentieren, dass make_unique erst in C++14 kommt und man daher nur bei shared_ptr vorerst einen sicheren Wrapper um den new -Aufruf hat.



  • Ok da war viel neues dabei, danke!
    also werde ich funktionsaufrufe jetzt ohne den shared_ptr machen und ich muss mir etwas für mein problem überlegen. wenigstens weiß ich jetzt schon worran es liegt.

    Wenn ich einen String* habe, den ich an eine klasse übergeben mölchte, die diesen auch speichert, um den inhalt des strings zu bearbeiten. wie realisiere ich das ohne rohen pointer?
    Es heißt ja, es geht alles ohne rohe pointer.

    Weil ich weiß leider nicht wie ich einen shared_ptr von diesem string bekommen kann. wenn nicht mit shared_ptr<CString>(&m_TXTConsole);



  • Es heißt ja, es geht alles ohne rohe pointer.

    Nein.

    Wenn ich einen String* habe, den ich an eine klasse übergeben mölchte, die diesen auch speichert, um den inhalt des strings zu bearbeiten.

    Wurde der String auf dem Heap alloziert? Oder ist es ein Zeiger auf ein Stackobjekt? Wenn die Klasse den Besitz übernimmt, dann tut das auch die Funktion. ➡ Smart-Pointer.



  • Gemischt mit RAW-Pointern und Smartpointern zu arbeiten ist nicht ganz trivial. Da gibt es einige Fallstricke.

    Wenn Du das wirklich tun möchtest würde ich Dir das nur für eine Übergangszeit empfehlen bis das komplette Projekt auf Smartpointer umgestellt ist. Schau Dir boost::enable_shared_from_this und die Möglichkeiten eigene deleter zu erstellen mal an.
    Damit kann man ganz nette Sachen machen. z.B. kannst Du wenn du irgendwo einen shared_ptr eines Objektes hast feststellen ob an anderer Stelle der Raw-Pointer mit delete zerstört wurde 😉

    Ganz wichtig: Niemals zweimal einen shared_ptr mit dem gleichen RAW-Pointer konstruieren.
    shared_ptr selber kannst Du kopieren und zuweisen wie Du willst, deswegen heissen die auch Smartpointer 😃



  • Also, erstmal im Hinterkopf behalten: Alles bis auf die Grunddatentypen (int, long, bool, double) bei Funktionsaufrufen als konstante Referenz(const&) zu übergeben. Wenn du gute Gründe hast oder es so brauchst nimmst du eine normale Referenz oder erst eine Kopie.

    void print(Foo const& f){} // weil es wird nur gelesen, daher reicht const&
    
    void swap(Foo & a, Foo & b){} // die änderungen sollen ja nach außen sichtbar sein
    // daher &
    
    void bar(Foo tmpCopy){} // hier willst du explizit eine Kopie haben
    // du willst vllt diese Kopie verändern und lesen und und, aber du willst keine Änderungen nach außen lassen
    

    Zu deinem String*:
    Meinst du einen (const) char* oder wirklich einen std::string* ?
    zweiteres sollte so gut wie nie vorkommen, zumal du ja Zeiger vermeiden willst.

    Und du kannst in Funktionsaufrufe schon gernen einen shared_ptr verwenden, aber standardmässig sollte dies ein Call-by-(Const)-Reference sein.



  • cl90, wenn Du "Klasse" sagst, meinst du manchmal offensichtlich "Objekt". Verwechsel diese zwei Dinge nicht.

    cl90 schrieb:

    shared_ptr<_Console> pcons(new _Console());
    

    _Console gehört zu den reservierten Namen, die du nicht verwenden darfst. Vermeide einfach alles mit zwei aufeinanderfolgenden Unterstrichen sowie Namen, die mit Unterstrich anfangen und mit einem Großbuchstaben weitergehen.
    Außerdem solltest Du das durch

    auto pcons = make_shared<Console>();
    

    ersetzen. Das ist effizienter so.

    cl90 schrieb:

    Aber was mache ich mit bereits initialiserten Variablen?
    bzw macht sowas Sinn:

    void _Console::find_command(shared_ptr<string> str)
    {
    	//doSth
    }
    

    Oder kann/sollte man hier vlt. einfach eine referenz verwenden?
    Es wäre auch call by value möglich, es ging mir nur darum nicht 100 Zeichen zu schicken, sondern nur einen Pointer.

    Was Du hier machen solltest, hängt davon ab, was die Funktion machen soll. Ich kann mir Situationen vorstellen, wo das so, wie Du es geschrieben hast, sinnvoll ist. Ich kann mir aber auch vorstellen, wo das hier besser wäre:

    void Console::find_command(string const& str);
    

    cl90 schrieb:

    [...] Trotzdem bekomme ich beim Schließen des Programms, einen Heap Error.

    Dann verwendest du shared_ptr wahrscheinlich falsch.

    shared_ptr<int> foo = make_shared<int>(1729);
    shared_ptr<int> bar (foo.get()); // FALSCH!
    

    Hier wissen foo und bar nichts voneinander. Beide glauben, alleiniger Besitzer des int-Objekts zu sein. Es gibt zwei Referenzzähler. Das ist einer zuviel. Der erste shared_ptr von ihnen, der zerstört wird, zieht dem anderen das int-Objekt unter'm Arsch weg.

    shared_ptr<int> foo = make_shared<int>(1729);
    shared_ptr<int> bar = foo; // RICHTIG!
    

    Hier gibt es nur einen einzigen Referenzzähler und bei der Kopie wird ordnungsgemäß dieser eine Zähler erhöht.



  • cl90 schrieb:

    BOOL testDlg::OnInitDialog()
    {
    
    	m_pTXTConsole	= shared_ptr<CString>(&m_TXTConsole);
    	m_pCData		= shared_ptr<_CData>(new _CData(m_pTXTConsole));
    	m_pConsole		= shared_ptr<_Console>(new _Console(m_pCData));
    
    	return TRUE;
    }
    

    Du kannst nicht einfach nen shared_ptr auf eine MEMBERVARIABLE machen und dann erwarten dass es funktioniert.
    Oder würdest du delete &m_TXTConsole; schreiben und dich dann wundern wenn es knallt? Nein? Wieso versuchst du das selbe dann mit nem Smart-Pointer?



  • Skym0sh0 schrieb:

    void print(Foo const& f){} // weil es wird nur gelesen, daher reicht const&
    
    void swap(Foo & a, Foo & b){} // die änderungen sollen ja nach außen sichtbar sein
    // daher &
    
    void bar(Foo tmpCopy){} // hier willst du explizit eine Kopie haben
    // du willst vllt diese Kopie verändern und lesen und und, aber du willst keine Änderungen nach außen lassen
    

    Danke!
    Und auch an alle anderen, ich habe mir hier vieles abgeguckt und mommentan gibt es nur eine Situation in der ich noch nicht um den raw pointer gekommen bin. ansonsten verwende ich shared, oder wie von Sky die ref/const ref.

    Worum ich bisher nicht gekommen bin:

    m_pCData->Attach_Dialog(this);
    

    Es geht um einen Observeranschluss. Alles andere hat bisher nicht funktioniert.
    m_pCData ist ein shared_ptr<_CData>



  • Oha!

    shared_ptr ist nicht dazu da, nicht mehr über Besitzverhältnisse nachzudenken zu müssen. shared_ptr ist dazu dar, sich das explizite delete zu sparen, nachdem man zum Schluss gekommen ist, dass ein geteilter Besitz (shared ownership) das Richtige ist.

    cl90 schrieb:

    mommentan gibt es nur eine Situation in der ich noch nicht um den raw pointer gekommen bin. ansonsten verwende ich shared, oder wie von Sky die ref/const ref.

    raw pointer sind nicht per se schlecht! Nutze einfach das, was die Besitzverhältnisse korrekt ausdrückt! Du scheinst shared_ptr einfach so zu nutzen, ohne das mit den Besitzverhältnissen verstanden zu haben.



  • Ja ich denke es gibt immer noch Dinge gibt die ich nicht oder nicht richtig weiß.

    Aber ich hab die besitzverhältnisse zu shared_ptrn schon verstanden.

    bla()
    {
    	int* i = new int(10);		// wird gelöscht sobald } erreicht wird
    
    	shared_ptr<int> j(new int(10));	// wird auch gelöscht sobald } erreicht wird
    
    	foo(j);			// sofern foo j irgendwo speichert wird das neue int nicht bei } gelöscht. 
    }
    

    So habe ich die shared_ptr verstanden. Die letzte instanz des pointers löscht das object. alle anderen nur den pointer den sie haben.



  • cl90 schrieb:

    bla()
    {
    	int* i = new int(10);		// wird gelöscht sobald } erreicht wird
    

    Nein, wird nicht gelöscht.



  • Ah ok. also muss ich für diesen Fall explizit delete aufrufen.
    Danke, hab ich in meinem Beispiel nicht durchdacht.



  • MichelRT schrieb:

    Gemischt mit RAW-Pointern und Smartpointern zu arbeiten ist nicht ganz trivial. Da gibt es einige Fallstricke.

    Und ob es trivial ist. Immer wenn ein Zeiger sein Objekt besitzt, nimmst du Smart-Pointer. Ansonsten nimmst du rohe Zeiger.

    MichelRT schrieb:

    Wenn Du das wirklich tun möchtest würde ich Dir das nur für eine Übergangszeit empfehlen bis das komplette Projekt auf Smartpointer umgestellt ist.

    Nein, rohe Zeiger sind durchaus berechtigt, wenn du sie für Verweise benutzt.

    MichelRT schrieb:

    Schau Dir boost::enable_shared_from_this und die Möglichkeiten eigene deleter zu erstellen mal an.
    Damit kann man ganz nette Sachen machen. z.B. kannst Du wenn du irgendwo einen shared_ptr eines Objektes hast feststellen ob an anderer Stelle der Raw-Pointer mit delete zerstört wurde 😉

    Klingt nach grober Frickelei. In einem vernünftigen Design ist sowas nicht nötig (siehe meinen ersten Satz).

    shared_ptr ist überbewertet. In den allermeisten Fällen braucht man ihn nicht. Nehmt bitte unique_ptr wenn ihr Smart-Pointer braucht, und shared_ptr nur in exotischen Fällen, wo tatsächlich geteilter Besitz notwendig ist (d.h. wo nicht im Voraus ein eindeutiger Besitzer festgestellt werden kann). shared_ptr aus Faulheit zu nehmen wird sich schnell rächen.

    cl90 schrieb:

    Ah ok. also muss ich für diesen Fall explizit delete aufrufen.
    Danke, hab ich in meinem Beispiel nicht durchdacht.

    Das ist aber so ziemlich das Grundlegendste von Speicherverwaltung! Bevor du Smart-Pointer verwendest, solltest du dir dieses Thema definitiv nochmals genau anschauen.

    <💡>



  • Mich würde mal interessieren wie viele der Leute die vor übermässigem Einsatz von shared_ptr warnen damit bereits selbst auf die Fresse gefallen sind.

    Ich verwende shared_ptr seit vielen Jahren sehr freizügig, und hatte damit bisher noch kaum ernste Probleme.
    Bisher genau einen Fall wo ich mir im Nachhinein gedacht habe dass es ziemlich sicher besser gewesen wäre unique_ptr zu verwenden.
    (Was in dem Projekt aber sowieso nur sehr umständlich möglich gewesen wäre, da kein Move-Support.)

    Daher gehe ich davon aus diese Sache - wie so vieles - einfach gerne nachgeplappert wird. Ohne dass die Leute, die nie müde werden es überall zu posten, wirklich selbst Erfahrung damit hätten.



  • glühbirne schrieb:

    MichelRT schrieb:

    Gemischt mit RAW-Pointern und Smartpointern zu arbeiten ist nicht ganz trivial. Da gibt es einige Fallstricke.

    Und ob es trivial ist. Immer wenn ein Zeiger sein Objekt besitzt, nimmst du Smart-Pointer. Ansonsten nimmst du rohe Zeiger.

    Mit "gemischt" meine ich Raw-Zeiger und shared_ptr auf das gleiche Objekt. 😃

    Wenn man etwas neu macht ist es natürlich grober Unfug und das Vorgehen ist da klar.



  • In der Tat habe ich noch nicht viele schlechte Erfahrungen gemacht mit dem shared_ptr. Aber eine schon, da hats mir aufgrund von zyklischen Abhängigkeiten einiges zerschossen und ich durfte debuggen und suchen und machen und tun bis ich nur diese Abhängigkeit entdeckt habe. Und dann musste ich nochmal gut nachdenken, wer wo was jetzt Sinn macht, wer den Besitz inne hält. Den bzw. die anderen Pointer konnten danach auf weak_ptr umgestellt werden.

    Aber es ist im Prinzip das alte leidliche Problem:
    "mh, shared_ptr, klappt, macht alles automatisch, super muss ich nix machen."
    Aber dass da die Zuständigkeiten und sowas geändert werden könnten, oder von anfang an nicht klar sind, das merken kaum welche.



  • hustbaer schrieb:

    Ich verwende shared_ptr seit vielen Jahren sehr freizügig, und hatte damit bisher noch kaum ernste Probleme.

    Die Frage ist, was du als Probleme betrachtest. Bis sich die Performance von Reference-Counting/Threadsicherheit bemerkbar macht, braucht es natürlich schon etwas. Das meinte ich nicht primär.

    Wesentlicher finde ich allerdings das, was krümelkacker angesprochen hat: Man denkt weniger über Besitzverhältnisse nach oder betrachtet shared_ptr als GC-Ersatz.

    Was ich z.B. schon gesehen habe, ist dass Relationen gänzlich mit shared_ptr modelliert werden, obwohl gar kein geteilter Besitz notwendig ist. Z.B. Objekt B muss A referenzieren. Natürlicherweise würde A genug lange leben und man kann ohne Probleme einen Verweis A* in B speichern. Aber es wird shared_ptr<A> genommen, weil das ja praktisch ist. Blöderweise kann man jetzt keine automatischen Objekte mehr referenzieren. Ah, kann man ja doch, indem man einen Custom-Deleter schreibt, der nichts tut (jedoch verlieren wir dabei die Garantie der starken Referenz). Meine Erfahrung war, dass solche Designs schlussendlich komplexer waren als wenn man die naheliegendste Lösung genommen hätte.

    Anderes Beispiel ist das massiv überbenutzte vector<shared_ptr<X>>, obwohl man fast immer vector<unique_ptr<X>> oder ptr_vector<X> möchte.

    hustbaer schrieb:

    Daher gehe ich davon aus diese Sache - wie so vieles - einfach gerne nachgeplappert wird.

    Ich persönlich habe das Gefühl, dass in dem Zusammenhang viel eher wie wahnsinnig gesagt wird, man müsse unter allen Umständen Smart-Pointer benutzen, ohne zu differenzieren. Leute glauben, RAII verstanden zu haben, und benutzen nur noch shared_ptr. Dabei ist die ursprüngliche Absicht von shared_ptr auf einen spezifischen Anwendungsfall ausgelegt.

    Hängt wahrscheinlich auch damit zusammen, dass sogar Boost selbst shared_ptr als Allheilmittel propagiert (sie benutzen ihn sogar für Pimpl). Vielleicht auch, weil scoped_ptr extrem eingeschränkt ist -- hier stehts um unique_ptr ja mittlerweile besser.

    Ich will shared_ptr nicht schlechtreden, ich brauche ihn selbst ab und zu. Gerade in Verbindung mit weak_ptr kann er wahnsinnig praktisch sein. Aber für den Thread hier, wo nicht mal new/delete verstanden wurde, ist shared_ptr ziemlich sicher nicht das Richtige. Man sieht auch schön die Auswüchse ("ich konnte leider noch nicht alle rohen Zeiger entfernen"). Benötigt wird eigentlich nur RAII, und das geht auch mit automatischen Objekten (oder unique_ptr wenn nötig).

    <💡>



  • MichelRT schrieb:

    Mit "gemischt" meine ich Raw-Zeiger und shared_ptr auf das gleiche Objekt. 😃

    Bei shared_ptr ist es eher unüblich, rohe Zeiger auf das Objekt zu haben, von Funktionsparametern mal abgesehen. Wenn man eine schwache Referenz benötigt, nimmt man weak_ptr.

    Mit unique_ptr hingegen ist es wirklich einfach: unique_ptr<X> hält das Objekt am Leben, und diverse X* sind einfach passive Verweise darauf. Ist nichts anderes als wenn man direkt X und X* hätte.

    <💡>


Log in to reply