Smart Pointer und Destruktoren



  • Hallo,

    ich bin gerade dabei C++(11) zu lernen und hänge jetzt bei der automatischen Speicherverwaltung fest.

    Ich habe einen Graph und möchte beliebige Knoten des Graphen referenzieren können, mir aber möglichst wenig Probleme mit der Speicherverwaltung machen. Habe mir gedacht ich gebe einfach einen smart Pointer raus wenn ich auf einen Knoten zugreifen möchte. Dabei möchte ich aber jederzeit in der Lage sein auf einen beliebigen Knoten des Graphen den dtor aufrufen zu können, der dann alle Kindknoten aufräumt sofern keine weitere Referenzen mehr darauf existieren.

    Folgendes minimale Beispiel veranschaulicht denke ich mein Problem, gibt aber einen segfault:

    #include <iostream>
    #include <memory>
    using namespace std;
    
    class A {
    public:
    
      virtual ~A() {
        cout << "dtor: A(" << this << ")" << endl;
      }
    
      virtual shared_ptr<A> inner() = 0;
    };
    
    class C : public A {
    private:
      A* a;
    
    public:
      C(A* a): a(a) {}
    
      ~C() {
        cout << "dtor: C(" << this << ")" << endl;
        delete a;
      }
    
      shared_ptr<A> inner() {
        return shared_ptr<A>(a);
      }
    };
    
    class V : public A {
    private:
      int v;
    
    public:
      V(int v): v(v) {}
    
      ~V() {
        cout << "dtor: V(" << this << ")" << endl;
      }
    
      shared_ptr<A> inner() {
        return shared_ptr<A>(new V(v));
      }
    };
    
    int main() {
      shared_ptr<A> c(new C(new C(new V(5))));
      auto inner = c->inner(); // segfault
    
      return 0;
    }
    

    Das Problem ist klar, ich habe mehrere smart Pointer auf einen Knoten, in dem Fall das innere C, die beide versuchen den Speicher wieder freizugeben). Ich habe aber keine Ahnung wie ich das Problem möglichst elegant löse.

    Soll ich den Pointer aus C zu einem smart Pointer ändern? Soll ich selbst ein reference counting implementieren und das im dtor von C beachten (oder gar nicht erst delete auf den Pointer aufrufen)? Soll ich inner() gar keinen smart Pointer zurückgeben lassen? Ist die Erzeugung eines Objekts in V.inner() ein Problem das ich anders lösen soll? Wäre schön wenn mir jemand sagen könnte was hier best practice ist.



  • Natürlich kannst du nicht einfach mal so ein paar Smartpointer einstreuen.

    Warum liefert inner einen Smartprointer? Soll der Aufrufer tatsächlich das Objekt besitzen, oder wird es nur kurzzeitig verwendet?

    Bei Bäumen kann man Smartpointer für die Kinder verwenden, bei allgemeinen Graphen, die Zyklen enthalten, ist das nicht so gut: A besitzt B, B besitzt A - wann sollte das jemals gelöscht werden?



  • Momentan reicht mir ein Baum wohl aus, habe also keine Zyklen. Ich bin mir nicht sicher ob der Aufrufer das Objekt besitzen soll. Ich möchte an sich nur Objekte in inner() erzeugen oder auf bereits existierende Objekte verweisen können. Und dafür suche ich ein geeignetes Modell, bei dem der Aufrufer sich nicht explizit um die Speicherfreigabe kümmern muss.



  • Das "Graph"-Objekt besitzt alleinig seine Knoten. Wenn du keines hast, fuehr eines ein.



  • Antoras schrieb:

    Ich möchte an sich nur Objekte in inner() erzeugen oder auf bereits existierende Objekte verweisen können. Und dafür suche ich ein geeignetes Modell, bei dem der Aufrufer sich nicht explizit um die Speicherfreigabe kümmern muss.

    Dann kannst du für neu erzeugte Objekte einen normalen shard_ptr zuruckgeben. Für existierende Graphobjekte könntest du einen shared_ptr mit einer Deleterklasse, die nichts löscht, erzeugen.



  • bei dem der Aufrufer sich nicht explizit um die Speicherfreigabe kümmern muss.

    Glaub der Ansatz ist Nobel, aber die Durchführung ist ....
    Egal was und wie du es tust, Du wirst in andere Probleme reinlaufen.

    Und es ist besser wenn der Anwender deiner Lib/klassen ... sich gedanken um die Laufzeit seiner Objekte macht/machen muss, weil nur er kann das auch effektiv entscheiden. Das ist eigentlich auch das Alleinstellungsmerkmal, sozusagen der Fluch und Segen von C/C++ zugleich :-). Ja man muss mehr aufpassen, aber dafuer kann man auch mehr optimieren und gezielter ressourcen belegen und performanter sein, und Dinge tun die in anderen Programmiersprachen eben nicht (so gut) gehen ^^

    BTW, wenn du deine "Lib" mal als dynamisch Lib (.dll .so ) distributieren willst, sind Templates und Smartpointer ganz speziell im Interface das SorgenKind Im Abhängigkeitsmanagment schlechthin.

    Ciao ...



  • Antoras schrieb:

    Das Problem ist klar, ich habe mehrere smart Pointer auf einen Knoten, in dem Fall das innere C, die beide versuchen

    Aus welchem Buch lernst Du?
    (Will schauen, ob es einen verantwortlichen Auto für diese Epidemie gibt.)



  • volkard schrieb:

    (Will schauen, ob es einen verantwortlichen Auto für diese Epidemie gibt.)

    Ja, mindestens einen gibt es: http://herbsutter.com/elements-of-modern-c-style/



  • manni66 schrieb:

    volkard schrieb:

    (Will schauen, ob es einen verantwortlichen Auto für diese Epidemie gibt.)

    Ja, mindestens einen gibt es: http://herbsutter.com/elements-of-modern-c-style/

    Ich habe mir in letzter Zeit tatsächlich einiges von Herb Sutter angeschaut. Den Eindruck den ich dabei bekommen habe ist, dass C++11 nicht mehr nur ein C mit viel Syntax Zucker ist, sondern dass es mehr "managed language like" wird. Ich wollte deshalb auch gleich mit C++11 einsteigen, jetzt sieht es aber eher so aus wie wenn das keine so gute Idee ist. Ein bestimmtes Buch habe ich keines, dafür aber wohl die falsche Erwartungshaltung.

    Wenn man sich in letzter Zeit so umschaut, dann wird einem überall gesagt, dass man kein new und delete mehr verwenden muss. Wenn man das eher verstehen muss im Sinne von "man muss kein new und delete mehr verwenden wenn man bereits C++ Profi ist", dann ist das ok, dann brauche ich von C++11 nämlich nichts erwarten was ich nicht bekommen kann.

    Soll ich lieber mit C ähnlichem Code anfangen, der C++ Header verwendet und mich dann nach oben hangeln und dabei C++11 Features möglichst außen vor lassen?



  • Wie hast du nur diesen Eindruck bekommen? Es ist sehr schwer jedes Werkzeug von C++ zu beherrschen. Es ist aber sehr einfach sich ein Problem zu nehmen und die passenden Werkzeuge (~5%) zu benutzen.
    Smartpointer sind einfach, eigentlich ist praktisch alles von C++11 auf Vereinfachung getrimmt. Nimm einfach shared_ptr in deinem Baum nachdem du verstanden hast was sie tun. Ist nicht so schwer, sie löschen die Resource nachdem der letzte shared_ptr weg ist. Damit kann man ganz leicht korrekte Programme schreiben.
    Später, wenn dir ein paar Nanosekunden zum Erfolg fehlen, dann kannst du rohe Pointer mit new und delete zu benutzen versuchen. Aber erst wenn du C++-Profi bist, davor nutze lieber den Komfort von C++11.



  • Antoras schrieb:

    Wenn man sich in letzter Zeit so umschaut, dann wird einem überall gesagt, dass man kein new und delete mehr verwenden muss.

    Ja, diesen Eindruck erweckt unser Forum. Hab's auch befürchtet, daß wir die Ursache sind und kein neues Buch.

    Vielleicht sollte man auch mal rohe Zeiger üben dürfen, geht aber nicht, verwendet man rohe Zeiger und fragt was, wird man sofort angebrüllt.

    Antoras schrieb:

    Soll ich lieber mit C ähnlichem Code anfangen, der C++ Header verwendet und mich dann nach oben hangeln und dabei C++11 Features möglichst außen vor lassen?

    Nee, macht wohl noch viel weniger Sinn. Aber woher wissen, was ein Luftschloss ist und was nicht? Naja, mehr als die übermäßige Verwendung von smart Pointers sehe ich zur Zeit nicht. Seltsamerweise werden die lamdas nicht völlig übertrieben. Weiß nicht, was ich allgemein empfehlen soll.

    Lass es vielleicht unter der Haube lockerer zugehen. Also innerhalb des Containers fühle Dich viel freier, den kannste ja gegen Bedienfehler abschließen.



  • Also, ich habe jetzt verschiedene Möglichkeiten ausprobiert, keine führte zum Erfolg.

    Zuerst habe ich alle pointer durch shared_ptr ersetzt, dabei bekam ich aber segfaults, da irgendwann mehrere shared_ptr auf den gleichen Speicherbereich gezeigt haben, das Problem habe ich aber schon im Ausgangspost angesprochen. Da ich keine Ahnung hatte wie ich das lösen sollte bin ich wieder auf normale Pointer umgestiegen, jetzt gebe ich aber immer zu wenig Speicher frei. Kleines Beispiel:

    #include <iostream>
    #include <memory>
    using namespace std;
    
    enum AType {
      CT, DT, VT
    };
    
    class A {
    public:
    
      virtual ~A() {
        cout << "dtor: A(" << this << ")" << endl;
      }
    
      virtual AType typeOf() = 0;
    };
    
    class C : public A {
    private:
      A* a;
    
    public:
      C(A* a): a(a) {}
    
      ~C() {
        cout << "dtor: C(" << this << ")" << endl;
        delete a;
      }
    
      AType typeOf() { return CT; }
    
      A* value() { return a; }
    };
    
    class D : public A {
    private:
      A* a;
    
    public:
      D(A* a): a(a) {}
    
      ~D() {
        cout << "dtor: D(" << this << ")" << endl;
        delete a;
      }
    
      AType typeOf() { return DT; }
    
      A* value() { return a; }
    };
    
    class V : public A {
    private:
      int v;
    
    public:
      V(int v): v(v) {}
    
      ~V() {
        cout << "dtor: V(" << this << ")" << endl;
      }
    
      AType typeOf() { return VT; }
    
      int value() { return v; }
    };
    
    A* inner(A* a) {
      switch (a->typeOf()) {
        case CT:
          return ((C*) a)->value();
        case DT:
          return new D(((D*) a)->value());
        case VT:
         return a;
      }
    }
    
    int main() {
      shared_ptr<A> c(new C(new D(new V(5))));
      auto x = inner(c.get());
      auto y = inner(x);
    
      return 0;
    }
    

    Da ich es nicht hinbekommen habe mit Speicherreservierung in polymorphen Methoden umzugehen, habe ich die Methode inner() einfach ausgelagert (was zwar zu neuen Problemen geführt hat aber das ist mir jetzt erst mal egal).

    Man sieht in inner() meine benötigten Anwendungsfälle. Ich muss Speicher reservieren oder aber einen pointer auf einen bereits existierenden Bereich zurückgeben. Ich habe keine Ahnung wie ich den reservierten Speicher wieder freigeben soll. Wenn ich inner() nur mit shared_ptr arbeiten lasse habe ich dagegen wieder das Problem zu viel Speicher frei zu geben. Wie löse ich das Problem jetzt am besten?



  • Ich verstehe nicht was du eigentlich tun willst. Erkläre mal, warum class C sowohl ein A ist, als auch ein A hat.

    Ich fand deinen ersten Ansatz sehr viel besser als deinen zweiter und habe daran etwas rumgeschraubt. Besonders zu erwähnen ist Zeile 21, wo du manuell a gelöscht hast und später shared_ptr a gelöscht hat, was zu deinem Fehler führte. Weiterhin habe ich in Zeile 15 aus A einen shared_ptr gemacht. Ich verstehe den Sinn des Programms noch nicht, aber das sollte dich auf die richtigen Ideen bringen.

    #include <iostream> 
    #include <memory> 
    using namespace std;
    
    class A {
    public:
    	virtual ~A() {
    		cout << "dtor: A(" << this << ")" << endl;
    	}
    	virtual shared_ptr<A> inner() = 0;
    };
    
    class C : public A {
    private:
    	shared_ptr<A> a;
    public:
    	C(A* a) : a(a){
    	}
    	~C(){
    		cout << "dtor: C(" << this << ")" << endl;
    		//delete a; //hier war der Fehler! Du hast a gelöscht und der shared_ptr auch!
    	}
    	shared_ptr<A> inner() override{
    		return a;
    	}
    };
    
    class V : public A {
    private:
    	int v;
    public:
    	V(int v) : v(v) {}
    	~V() {
    		cout << "dtor: V(" << this << ")" << endl;
    	}
    	shared_ptr<A> inner() override{
    		return shared_ptr<A>(new V(v));
    	}
    };
    
    int main() {
    	shared_ptr<A> c(new C(new C(new V(5))));
    	auto inner = c->inner(); //nix mehr segfault 
    }
    


  • class A soll einen Baum symbolisieren, z.B. einen AST für mathematische Ausdrücke. inner() könnte eine simplify-Funktion sein, die z.B. 5*1 in 5 umwandelt.

    Dein Lösungsvorschlag hatte ich auch schon ausprobiert, er funktioniert aber nicht wenn du das 'a' aus Zeile 24 durch 'shared_ptr<A>(this);' ersetzt (das würde z.B. bei dem Ausdruck 5+1 eintreten, wenn dieser nicht weiter zu 6 vereinfacht werden soll).



  • Antoras schrieb:

    Dein Lösungsvorschlag hatte ich auch schon ausprobiert, er funktioniert aber nicht wenn du das 'a' aus Zeile 24 durch 'shared_ptr<A>(this);' ersetzt (das würde z.B. bei dem Ausdruck 5+1 eintreten, wenn dieser nicht weiter zu 6 vereinfacht werden soll).

    Naja, man darf halt nur Sachen dem shared_ptr geben, die der shared_ptr auch löschen soll. Aber ich verstehe das Problem.

    Eine mögliche Lösung ist eine Kopie zu machen:

    class C : public A {
    private:
        shared_ptr<A> a;
    public:
        C(A* a) : a(a){
        }
        explicit C(C &other) : a(other.a){
        }
        ~C(){
            cout << "dtor: C(" << this << ")" << endl;
            //delete a;
        }
        shared_ptr<A> inner() override{
            return C(*this); //Zeile 24 //nicht das * vergessen, sonst gibts segfaults!
        }
    };
    

    Es fühlt sich alles andere als optimal an.



  • Ich sollte auch schreiben was ich meine. Zeile 24 muss *return new C(this); heißen.



  • Komplett richtig ist wohl 'shared_ptr<A>(new C(*this));' 😉

    Das funktioniert jetzt endlich mal, danke!

    Ich habe mir schon gedacht, dass es irgendwie auf zwei Lösungen rausläuft: Entweder alle neu benötigten Teiles des Baums kopieren, oder aber sich irgendwie eine kleine mini Speicherverwaltungseinheit schreiben, die sich merkt was schon frei gegeben ist und was nicht und dann nur noch über diese Speicherverwaltungseinheit den Speicher freigeben lassen. Ersteres ist nicht effizient, aber für meine Zwecke vorerst ausreichend. Und letzteres ist mir zu aufwendig zu implementieren.

    Btw, wofür wird der Stern in 'new C(*this)' benötigt?



  • Der Stern dereferenziert den this Pointer. So wird der Kopierkonstruktor von C aufgerufen.



  • Weil Klasse C zwei Konstruktoren hat. Einer nimmt ein *A a, was ein neues C als Elternknoten von dem übergebenen a im Baum erzeugt, der andere nimmt ein C &c, was eine Kopie des Knoten machen soll. Das this ist ein *A **, also würde inner bei return new C(this) einen neuen Elternknoten von sich selbst erzeugen, während *this ein C ist (kein Zeiger) und eine Kopie erzeugt. Das sollte man unbedingt auseinander halten. Es sollte einen Syntaxfehler geben wenn man es falsch macht, wahrscheinlich sollte man statt eines Konstruktors lieber eine Memberfunktion copy oder so benutzen, damit man das Sternchen nicht vergessen kann.



  • evtl hilft dir auch eine fertige implementierung für kopierbare smart-pointer wie zb diese hier. mit shared_ptr würde ich nur dinge verwalten, die tatsächlich geteilt werden.


Log in to reply