Smartpointer-Hype vs. Over-Dev



  • Hallo, an vielen Ecken hört man "nimm Smartpointer" - oftmals mit der schmalen Begründen, sie seien "besser". Vor einiger Zeit hatte ich mit einem Bekannten darüber ein Gespräch geführt in dem zwei Welten aufeinander prallten.

    Zum Hintergrund: Im Moment arbeite ich (Hobbybereich) an einem 2D RPG in dem ich verschiedene GameObjects und verschiedene Dungeons habe, wobei ein GameObject zu einem Zeitpunkt zu höchstens einem Dungeon gehört. Zusätzlich habe ich einen GameContext, der sämtliche GameObjects und Dungeons verwaltet, d.h. über den mittels Factory neue GameObjects und Dungeons erzeugt werden. Die Dungeons werden konkret im Context gespeichert, die GameObjects nur indirekt innerhalb der Dungeons. Quasi besitzt der Context die Dungeons und die Dungeons besitzen die GameObjects, wobei die Zugehörigkeit der GameObjects zwischen den Dungeons wechseln kann.

    Im Moment verwende ich ausschließlich raw Pointer und viele lvalue Referenzen. Das heißt konkret:

    bool Dungeon::spawn(GameObject& obj)
    

    speichert sich dann intern den Zeiger &obj .

    std::vector<GameObject*> objects // <-- vereinfacht, Rest lass ich weg ^^
    

    Oder auch

    void GameObject::setDungeon(Dungeon& dungeon)
    

    speichert sich den Zeiger &dungeon .

    Dungeon* dungeon
    

    Möchte ich ein GameObject zerstören, markiere ich es als destroyed , so dass es beim nächsten update des Dungeons von diesem "abgestoßen" wird, d.h. es entfernt das Objekt aus seiner Liste und löscht es explizit.

    Analog verwende ich das Component-Pattern für die GameObjects, wobei jede Komponente selbst aber eine Referenz auf ihren Eigentümer besitzt, also

    MyComponent(GameObject& parent) // als ctor
    
    GameObject& parent // private member
    

    Probleme wären also die Fälle, wenn ein GameObject (warum auch immer) von keinem oder mehr als einem Dungeon "besitzt" werden würde (Leak bzw. Multiple delete). Das verhindere ich (semantisch), indem ich nie direkt die setDungeon -Methode eines GameObjects aufrufe, sondern mit spawn und (der analogen) vanish -Methode der Dungeons arbeite - wobei diese prüfen, ob das GameObject überhaupt spawnen/vanishen darf (Stichwort: "ist noch wo anders" bzw. "ist gar nicht hier").

    Nun zum eigentlich Kern 😃 Ich wurde gefragt, warum ich dafür keine Smartpointer verwende. Meine Antwort war, dass ich klare Zuständigkeiten für Create und Destroy habe. Zugegeben: Das System ist schon nicht mehr ganz trivial - aber aus meiner Sicht auch noch nicht zu kompliziert.

    Generell werden Smartpointer im Moment ziemlich gehyped.. Ich versuche dabei immer die andere Seite der Medaille zu betrachten: std::unique_ptr eignet sich afaik nur, wenn ich mein Objekt nicht teilen will - und std::shared_ptr eben wenn ich es teilen will (dafür halt der kleine Counting overhead). Allerdings sehe ich keinen wirklichen Vorteil darin, jetzt alles mit Shared Pointern zu pflastern.. Ich vermute, dass ich damit viel mehr noch Probleme erzeuge wo eigentlich keine sind (also durch den "blinden" Einsatz - nur der Smartpointer wegen).

    Eigentlich bin ich ein Freund von raw Pointern und Referenzen.. Prinzipiell bin ich ja gut beraten zwischen "owning" und "non-owning" Pointern zu unterscheiden (d.h. z.B. das Dungeon besitzt einen "owning" Pointer auf ein GameObject - und darf dieses u.U. löschen; und das GameObject besitzt einen "non-owning" Pointer auf das Dungeon - und darf dieses nicht löschen). Solang ich nicht anfange zur Laufzeit Dungeons zu löschen, in denen sich GameObjects befinden, sollte ich ja ein relativ solides Design haben..
    Oder wie steht ihr dazu?

    LG Glocke



  • smart pointer verhindern, dass speicher leaked und das auch bei exceptions.

    Ich verwende immer smart pointer, wenn das object einen Besitzer hat und meistens ist der klar. Rohe pointer gibt es ja immer noch mit smart_pointer.get() und an Referenzen kommt man auch mit dereferenzieren, da muss man auch aufpassen, dass diese nicht mehr verwendet werden, nachdem das Objekt schon geloescht ist. Nur muss man den Speicher nicht mehr per Hand freigeben, was nicht nur sicherer ist, sondern auch wesentlich uebersichtlicher, weil eben nicht an jedem if ein eigenes delete steht.



  • weil eben nicht an jedem if ein eigenes delete steht.

    ???????????ßßß



  • Das ganze mit den Referenzen und Verweisen kannst du ja beibehalten.
    Du hast wohl irgendwo einen Container, der Zeiger hat, wo die Objekte mit new intitialisiert und mit delete zerstört werden. Diese Zeiger ersetzt du durch std::unique_ptr und schon ist das delete automatisiert. Mehr Smartpointer brauchst du nicht.



  • Nathan schrieb:

    Du hast wohl irgendwo einen Container, der Zeiger hat, wo die Objekte mit new intitialisiert und mit delete zerstört werden. Diese Zeiger ersetzt du durch std::unique_ptr

    Naja ich habe mehrere. Kurzer Abriss der Container:

    struct GameContext {
        // von factory mit emplace_back befüllt
        std::vector<Dungeon> dungeons;
    
        Dungeon& createDungeon(...); // dungeon factory
        GameObject* createObject(...); // object factory
    };
    
    struct Dungeon {
        struct Cell {
            std::set<GameObject*> objects;
            // und andere Daten pro Tile
        };
        // hier macht unique_ptr sinn, da rein dungeon-intern
        // prinzipiell könnte ein Tile auch nicht vorhanden sein (-> nullptr)
        std::vector<std::unique_ptr<Cell>> cells;
    
        void spawn(GameObject& object);
        void vanish(GameObject& object);
    };
    
    struct GameObject {
        // von setDungeon gesetzt
        Dungeon* dungeon;
        bool is_destroyed; // flag, damit Dungeon das GameObject löschen darf
    
        void setDungeon(Dungeon& dungeon);
    };
    

    wie meinst du das mit den unique pointern genau?

    LG Glocke

    /EDIT: Oder: Die Factory könnte einen std::unique_ptr<GameObject> liefern, den ich mittels std::move zwischen den Dungeons hin- und herschieben kann *grübel*



  • Marthog schrieb:

    smart pointer verhindern, dass speicher leaked und das auch bei exceptions.

    Ich verwende immer smart pointer, wenn das object einen Besitzer hat und meistens ist der klar. Rohe pointer gibt es ja immer noch mit smart_pointer.get() und an Referenzen kommt man auch mit dereferenzieren, da muss man auch aufpassen, dass diese nicht mehr verwendet werden, nachdem das Objekt schon geloescht ist. Nur muss man den Speicher nicht mehr per Hand freigeben, was nicht nur sicherer ist, sondern auch wesentlich uebersichtlicher, weil eben nicht an jedem if ein eigenes delete steht.

    Klingt wie "Verwende bitte Smartpointer, ach, ich mags auch nicht begründen.".
    Er braucht bei seinem Entwurf keine.



  • Bei diesem Design brauchst du keinen smart pointer, weil der Besitzer (also derjenige, der fuer die Speicherfreigabe zustaendig ist) GameContext ist und der hat die Objekte ja bereits im Vector. Der Speicher ist also schon gemanaged und unique_ptr wuerde lediglich eine zusaetzliche indirektion einfuehren, das Design also verschlechtern.
    Smartpointer ersetzen lediglich rohe, besitzende pointer, deren Lebenszeit man selber mit new und delete bestimmt.

    volkard schrieb:

    Klingt wie "Verwende bitte Smartpointer, ach, ich mags auch nicht begründen.".
    Er braucht bei seinem Entwurf keine.

    Weil er die Objekte direkt im Array speichert. Das ist natuerlich die Beste Methode und funktioniert, solange man nur begrenzt viele verschiedene Typen hat und sicher sein kann, dass der vector zwischendurch seinen Speicherbereich nicht veraendert.
    Von diesem Design konnte ich aber vorher nichts wissen.



  • Marthog schrieb:

    Weil er die Objekte direkt im Array speichert. Das ist natuerlich die Beste Methode und funktioniert, solange man nur begrenzt viele verschiedene Typen hat und sicher sein kann, dass der vector zwischendurch seinen Speicherbereich nicht veraendert.

    Was meinst du damit genau: [...] der vector zwischendurch seinen Speicherbereich nicht veraendert[...] ?



  • Glocke schrieb:

    Marthog schrieb:

    Weil er die Objekte direkt im Array speichert. Das ist natuerlich die Beste Methode und funktioniert, solange man nur begrenzt viele verschiedene Typen hat und sicher sein kann, dass der vector zwischendurch seinen Speicherbereich nicht veraendert.

    Was meinst du damit genau: [...] der vector zwischendurch seinen Speicherbereich nicht veraendert[...] ?

    Nun ein Vector hat intern einen via new oder so angeforderten Speicherblock. Fügst du Elemente ein, wird dieser irgendwann voll. Dann wird ein neuer angefordert, die Elemente rübergemoved und der alte freigegeben. Etwaige Zeiger auf die Elemente zeigen danach natürlich auf den alten Speicherblock und sind somit ungültig.
    Anders als bei einem vector von unique_ptr, dort zeigen die Zeiger auf einen eigenen Speicherbereich, der vom vector nicht verändert wird, sondern nur der, wo die Zeiger drin sind. Aber auch bei anderen Datenstrukturen bleiben sie gültig.



  • Beziehst du dich damit auf std::vector<Dungeon> dungeons ? D.h. das meine Dungeon-Pointer möglicherweise ungültig werden, wenn sich der Speicherbereich von std::vector ändert?

    /EDIT: Ich hatte eh vor std::unordered_map<unsigned short, Dungeon> zu verwenden um die Dungeons mit IDs ansprechen zu können - die unverändert bleiben wenn ich in der Mitte ein's rauswerfen will...



  • Glocke schrieb:

    Beziehst du dich damit auf std::vector<Dungeon> dungeons ? D.h. das meine Dungeon-Pointer möglicherweise ungültig werden, wenn sich der Speicherbereich von std::vector ändert?

    Ja.

    /EDIT: Ich hatte eh vor std::unordered_map<unsigned short, Dungeon> zu verwenden um die Dungeons mit IDs ansprechen zu können - die unverändert bleiben wenn ich in der Mitte ein's rauswerfen will...

    unordered_map garantiert, dass Pointer gültig bleiben (aber keine Iteratoren!).



  • Nathan schrieb:

    unordered_map garantiert, dass Pointer gültig bleiben (aber keine Iteratoren!).

    Genau das ist die Idee 🙂



  • @Glocke
    Löscht du jetzt irgend welche Objekte mit delete , oder wird alles über vector/map etc. gemacht?
    Denn wenn du irgendwo delete stehen hast, dann ist vermutlich genau das eine Stelle wo du besser nen Smartpointer verwenden würdest.



  • hustbaer schrieb:

    Denn wenn du irgendwo delete stehen hast, dann ist vermutlich genau das eine Stelle wo du besser nen Smartpointer verwenden würdest.

    - Die lifetime eines Dungeons wird komplett vom STL-Container verwaltet - da steht direkt das Dungeon drinne (kein Pointer).
    - Bei der lifetime von GameObjects ist es anders: Da diese zwischen Dungeons ausgetauscht werden, löscht das jeweilige Dungeon das Objekt mit delete .

    Wie würdest du das Design denn abändern? Der Weg den ich sehe wäre über std::shared_ptr<GameObject> , d.h. immer einen Shared-Pointer zu übergeben. Dann habe ich aber viele (interne) inkrements/dekrements... Allerdings stört mich: ich habe ja keine shared ownership im klassischen Sinne; die ownership wechselt vom einen Dungeon in das nächste.

    Im Moment überlege ich, ob ich das GameObject mit std::move zwischen den Dungeons bewege. Bei Calls (die das Objekt nicht "speichern") würde ich dann eine lvalue reference nehmen - oder einen non-owning raw pointer, wenn nullptr semantisch erlaubt sein soll... Bzw. einen std::unique_ptr<GameObject> mit std::move bewegen bzw. eine Referenz auf den Unique-Pointer zu übergeben - dann hätte ich das "null-able" gleich mit..



  • Glocke schrieb:

    Allerdings stört mich: ich habe ja keine shared ownership im klassischen Sinne; die ownership wechselt vom einen Dungeon in das nächste.

    Dann nimm unique_ptr .
    Du musst ja sowieso sicherstellen dass es keinen Zeitpunkt gibt wo nicht genau ein Dungeon existiert der sich zuständig dafür fühlen ein bestimmtes Objekt zu löschen.
    Wenn du unique_ptr nimmst wird das nur einfacher - da du die Problemstellen nicht mehr so leicht übersehen kannst.



  • hustbaer schrieb:

    Wenn du unique_ptr nimmst wird das nur einfacher - da du die Problemstellen nicht mehr so leicht übersehen kannst.

    Danke! Mir ist gerade noch etwas klar geworden: Ich habe eine Camera -Klasse die steuert, welcher Bildschirmausschnitt gezeichnet wird. Damit sie dem Spieler folgt, besitzt sie im Moment einen GameObject* raw Pointer.. das wäre mit den Unique Pointern allerdings ein Problem denke ich (oder zumindest kein gutes Design). Allerdings möchte ich auch keine Unique Pointer Referenz in der Camera ablegen - damit ich die Kamera (ggf. später) zwischen zwei Objekten wechseln lassen kann (z.B. um storytechnisch kurz auf einen Boss zu blicken und dann wieder zum Spieler zurück).

    Im Grunde bräuchte ich was, das sich einen Unique Pointer merkt, aber auch null sein kann, quasi std::unique_ptr<GameObject>* . Da das aber wiederum nicht schön ist (finde ich zumindest), habe ich überlegt diese "weak reference" zu kapseln, d.h. eine Klasse WeakRef<T> zu schreiben. Was haltet ihr von diesem Ansatz? (Konkrete Implementierung hab ich schon im Kopf - kann ich später ja mal posten).

    LG Glocke



  • Glocke schrieb:

    habe ich überlegt diese "weak reference" zu kapseln, d.h. eine Klasse WeakRef<T> zu schreiben. Was haltet ihr von diesem Ansatz? (Konkrete Implementierung hab ich schon im Kopf - kann ich später ja mal posten).

    Es gibt schon einen std::weak_ptr, da muss man nichts selber schreiben. Ich bezweifle sowiso, dass du so etwas brauchst. Im Allgemeinen verschwinden Objekte nicht spontan vor der Kamera.



  • Glocke schrieb:

    Damit sie dem Spieler folgt, besitzt sie im Moment einen GameObject* raw Pointer..

    Die Kamera muss von irgendjemandem erstellt worden sein. Der hat sich darum zu kümmern, dass der Zeiger gültig bleibt oder die Kamera verschwindet.



  • manni66 schrieb:

    Es gibt schon einen std::weak_ptr, da muss man nichts selber schreiben.

    Afaik existiert std::weak_ptr nur im Kontext von std::shared_ptr .

    manni66 schrieb:

    Ich bezweifle sowiso, dass du so etwas brauchst. Im Allgemeinen verschwinden Objekte nicht spontan vor der Kamera.

    TyRoXx schrieb:

    Die Kamera muss von irgendjemandem erstellt worden sein. Der hat sich darum zu kümmern, dass der Zeiger gültig bleibt oder die Kamera verschwindet.

    Ich denke wenn ich das ganze event driven aufbaue (was ich ohne hin schon mache und auch weiterhin vor habe - bin gerade am Code Refactoring^^), dann sollte das im Grunde auch kein Problem sein 🙂



  • Bitte, lese Dir etwas über das RAII (resource acquisition is initialization) durch.

    Rohe Pointer als Resource-Owner haben in 99,9% aller Fälle im Code seit C++11 schlicht und ergreifend nichts mehr verloren.

    Sie erhöhen die Chance von Fehlern und memory leaks einfach beträchtlich.
    Wenn auch noch Exceptions ins Spiel kommen, wird das ganze nur noch schlimmer und garstiger.

    Bjarne Stroustrup, Herb Sutter, Scott Meyers und die ganzen C++ Gurus, sowie die Leute vom C++ Standard Committee empfehlen Smartpointer zu verwenden, da sie schlicht ergreifend sicherer sind und von der Performance her rohen Pointern eben würdig sind. Die Sprache hat sich weiterentwickelt und sie ist vor allem sicherer geworden.

    Wenn also die klügsten Köpfe in Ihrem Fachgebiet und der Typ der die Sprache "erfunden" hat, Dir sagen benutze bitte diese neue "Verfahren" sollte man vielleicht mal seinen eigenen Standpunkt überdenken und sich fragen ob "Die Welt ist eine Scheibe!" / "Das habe ich schon immer so gemacht!" und "Früher war alles besser!!!" Herangehensweise die Richtige ist.


Anmelden zum Antworten