Smartpointer-Hype vs. Over-Dev



  • 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.



  • Genau, C++14 steht in den Startlöchern und der aktuelle C++ Standard ist seit Jahren C++11. Also nutze diesen auch. Niemand will noch altes C++ programmieren.



  • Glocke schrieb:

    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 .

    Ja, wenn man einen unique_ptr "sharen" will, hat man eien shared_ptr, nur in kompliziert und vermutlich fehlerhaft.



  • my two cents schrieb:

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

    Bitte lies dir den bisherigen Thread durch. Sorry, auch wenn es hart klingt - aber ich vermute das hast du bisher nicht getan.

    my two cents schrieb:

    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.

    Schön, dass jetzt jemand mit der "Raw Pointer sind Mist"-Keule kommt. Bei RAII geht es eben genau um die Initialisierung und auch die Freigabe! Wenn ich die Speicherverwaltung mit unique Pointern behandle, entspreche ich RAII!

    Meine Frage bezieht sich im Moment also nicht auf "owning" Pointer, sondern auf "non-owning" Pointer, d.h. "ich kenne das Objekt, jemand anderes besitzt es aber". Einen rohen Pointer im reinen "non-owning" Context zu verwenden, bringt mir keine Memory Leaks - auch nicht bei ausgelösten Exceptions - die Speicherverwaltung unterliegt immernoch dem RAII. Allerdings habe ich das Problem "ist der Pointer gültig/ungültig" dadurch noch am Hals.

    Und nur weil mein Design kein reines Smartpointer-Design ist, sehe ich noch keine ausreichende Begründung, dass es falsch oder veraltet sei.

    manni66 schrieb:

    Ja, wenn man einen unique_ptr "sharen" will, hat man eien shared_ptr, nur in kompliziert und vermutlich fehlerhaft.

    http://en.cppreference.com/w/cpp/memory/shared_ptr
    std::shared_ptr is a smart pointer that retains shared ownership of an object through a pointer.

    Ich habe aber keine shared ownership in meinem Design. Und "X verwendet Y" heißt nicht, dass X auch zwangsläufig Y besitzen muss!

    my two cents schrieb:

    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.

    Lies bitte auch das Startpost noch einmal! Dort habe ich klar gemacht, dass ich meinen Standpunkt überdenken möchte und explizit nach Feedback gefragt. Allerdings macht Feedback (vor allem wenn man ein konkretes Design als Diskussionsgrundlage verwendet) nur dann Sinn, wenn man seine Argumente auch begründet. "Nimm Smartpointer, die Profis nehmen sie doch auch.. Mensch wach endlich auf"-artige Argumentationen sind ... ehrlich gesagt absoluter Mist, sorry wenn ich dir damit zu nahe trete. Aber wirklich konstruktiv ist ein "die anderen"-Argument nie :p



  • Edit:

    Glocke schrieb:

    Allerdings habe ich das Problem "ist der Pointer gültig/ungültig" dadurch noch am Hals.

    Versteh, ich nicht.

    Es gibt zwei Möglichkeiten, entweder du hast einen Owner und möglicherweise mehrere Consumer.
    Der Owner wäre in dem Fall ein unique_ptr, der native Pointer an die Consumer vergibt.
    Die Logik muss gewährleisten, dass der Owner länger lebt als alle Consumer.

    Oder

    Du hast shared Ownership die Programmlogik kann nicht gewährleisten, dass der Owner länger lebt als die Consumer.
    Dann hast du zwei Möglichkeiten, du benutzt einen shared_ptr oder du movest den unique_ptr von einem Owner zu dem nächsten Owner.

    Unique_ptr move macht nur Sinn, wenn du praktisch wie beim Staffellauf die Ressource von einem zum Nächsten übergibst.



  • chp++ schrieb:

    [...] oder du movest den unique_ptr von einem Owner zu dem nächsten Owner.

    Unique_ptr move macht nur Sinn, wenn du praktisch wie beim Staffellauf die Ressource von einem zum Nächsten übergibst.

    Genau das ist die gewünschte Semantik: Staffellauf! Ein Dungeon gibt den unique_ptr an den nächsten weiter - oder das Objekt wird nur aus dem Dungeon entfernt - dann stirbt das Objekt mit der Scope (dank unique_ptr und RAII). Genau für diesen Fall muss ich sorgen, dass alle Consumer darüber informiert werden, so dass deren Zeiger auf nullptr gesetzt werden können. Ansonsten habe ich noch Zeiger die auf bereits freigegebene Objekte zeigen.

    Ich überlege gerade folgendes: Vielleicht wäre es (um keinen Consumer zu vergessen) hilfreich eine art Observer-Pattern zu verwenden: Der Consumer fordert das GameObject an (subscribe) und bekommt einen nativen Pointer (um bei deiner Terminologie zu bleiben). Sobald das ursprüngliche Objekt freigegeben wird (z.B. in einem customized deleter, den man dem unique Pointer anfangs mitgibt), werden dann alle customizer benachrichtigt, so dass diese ihre nativen Pointer auf nullptr setzen können. 🙂 Anmerkungen?



  • Glocke schrieb:

    Genau das ist die gewünschte Semantik: Staffellauf! Ein Dungeon gibt den unique_ptr an den nächsten weiter - oder das Objekt wird nur aus dem Dungeon entfernt - dann stirbt das Objekt mit der Scope (dank unique_ptr und RAII). Genau für diesen Fall muss ich sorgen, dass alle Consumer darüber informiert werden, so dass deren Zeiger auf nullptr gesetzt werden können. Ansonsten habe ich noch Zeiger die auf bereits freigegebene Objekte zeigen.

    Mich macht ein wenig stutzig, dass du Consumer hast die den Owner überleben.
    Kannst Du vielleicht nochmal kurz dein Setup erklären, so ganz habe ich es nicht verstanden.

    Was ist bei Dir ein Dungeon?
    Kann immer nur ein Dungeon am Leben sein?
    Was setzt du wo auf nullptr und was machen diese Consumer während des Gamezyklus?
    Warum willst du Objekte (was für Objekte?) von einem Dungeon zum nächsten übertragen.



  • Glocke schrieb:

    Ich überlege gerade folgendes: Vielleicht wäre es (um keinen Consumer zu vergessen) hilfreich eine art Observer-Pattern zu verwenden: Der Consumer fordert das GameObject an (subscribe) und bekommt einen nativen Pointer (um bei deiner Terminologie zu bleiben). Sobald das ursprüngliche Objekt freigegeben wird (z.B. in einem customized deleter, den man dem unique Pointer anfangs mitgibt), werden dann alle customizer benachrichtigt, so dass diese ihre nativen Pointer auf nullptr setzen können. 🙂 Anmerkungen?

    Kann man so machen, muss man aber nicht. Ich habe es eine Weile so gemacht und habe mehr Probleme geschaffen als gelöst. Auf einmal mussten Objekte auf das Verschwinden anderer Objekte mit mehr als p = nullptr; reagieren. Die Reaktion hat häufig weitere Zerstörungen oder sogar Erschaffungen bewirkt, die jeweils Benachrichtigungen auslösten. Da hat man ganz schnell stack overflow oder use-after-free, weil man irgendeinen Grenzfall vergessen hat.

    Vielleicht wäre eine weniger technische Lösung hier angebracht. Integriere das Verschwinden von Objekten ganz normal in die Spiellogik. Wenn jeder Consumer sich an die Regeln hält, die ihn betreffen, braucht man keine generische Lösung, die in manchen Fällen eben doch nicht funktioniert.



  • chp++ schrieb:

    Mich macht ein wenig stutzig, dass du Consumer hast die den Owner überleben.
    Kannst Du vielleicht nochmal kurz dein Setup erklären, so ganz habe ich es nicht verstanden.

    Jaein: Der Consumer (z.B. Kamera) überlebt das GameObject (was sich im unique Pointer befindet), aber nicht dessen Besitzer (das Dungeon). Daher muss der Consumer (Kamera) auf das Ableben des GameObjects reagieren können.

    chp++ schrieb:

    Was ist bei Dir ein Dungeon?

    Ein Dungeon besitzt 0..* GameObjects.

    chp++ schrieb:

    Kann immer nur ein Dungeon am Leben sein?

    Nein, mehrere.

    chp++ schrieb:

    Was setzt du wo auf nullptr und was machen diese Consumer während des Gamezyklus?

    Angenommen eine Instanz von Dungeon besitzt ein GameObject und eine Instanz von Camera soll dieses Objekt verfolgen. Dann würde das Dungeon den unique Pointer und die Kamera den raw Pointer haben.
    Sei das GameObject ein Feuerball, dem die Kamera gerade gefolgt ist. Der Feuerball verschwindet und die Kamera soll wieder zum Spieler springen. Der Feuerball wird zerstört, dann muss der raw Pointer in der Kamera auf den Spieler (oder vorrübergehend auf nullptr) gesetzt werden

    chp++ schrieb:

    Warum willst du Objekte (was für Objekte?) von einem Dungeon zum nächsten übertragen.

    Die GameObjects sind in erster Linie alle Objekte, die irgendwie interaktiv sind. D.h. Spieler und Gegner, aber auch Geschosse (Feuerbälle etc.) und Umgebungsobjekte wie Truhen etc. Nun macht es für Truhen absolut keinen Sinn sich in ein neues Dungeon zu bewegen; ob ein Feuerball das Dungeon verlassen darf sei auch mal dahin gestellt. In jedem Fall können (und sollen!) diese aus dem Dungeon verschwinden - man will ja nach ein paar Stunden spielen keine tausenden Feuerbälle im Speicher liegen haben.
    Auf jeden fall sollen Spieler und auch Gegner ein Dungeon (durch eine Treppe o.Ä.) verlassen können und in einem anderen Dungeon ankommen. Analog könnte man auch das Verschwinden der Gegnerleiche über das Zerstören des GameObjects realisieren.

    Ich hoffe ich habe es verständlich erklärt 🙂

    TyRoXx schrieb:

    Vielleicht wäre eine weniger technische Lösung hier angebracht. Integriere das Verschwinden von Objekten ganz normal in die Spiellogik. Wenn jeder Consumer sich an die Regeln hält, die ihn betreffen, braucht man keine generische Lösung, die in manchen Fällen eben doch nicht funktioniert.

    An der Stelle fällt mir halt nur ein Event-basierter Ansatz ein - vermutlich wäre es das beste mir ein Interface anzulegen, das alle die Implementieren, die auf das Ableben eines GameObjects reagieren müssen - und dann polymorph zu implementieren, wie jeder einzelne reagiert. Bei GameObjects habe ich - zur Kommunikation mit seinen Components einen ObjectEvent -Typ (wenn sich z.B. die Position ändert) - vllt. bietet sich analog ein DungeonEvent -Typ an - für Sachen eine Ebene darüber: Objekt erzeugt, Objekt zerstört usw.

    Da der Consumer an sich keine Möglichkeit hat, herauszufinden ob sein Zeiger noch gültig ist, muss er eigentlich darüber informiert werden, wenn der Zeiger ungültig wird. Die einfachste Lösung wäre also vermutlich, dass sich die Kamera beim Dungeon anmeldet und darüber informiert wird wenn (a) das Objekt zerstört wurde oder (b) das Objekt den Besitzer gewechselt hat (damit sich die Kamera beim neuen Dungeon anmelden kann). Oder habt ihr einen anderen Vorschlag? 🙂



  • Du könntest das Austragen aus der Kameraliste auch in den Destructor des Game_Objects packen oder an der Stelle an der entschieden wird ,dass das Objekt zerstört werden muss.
    Du könntest das dann auch von unten reinerben und bei Bedarf virtuell überschreiben.

    Du wärst in dem Fall auch sicher, dass das Objekt erst zerstört wird, wenn es aus der Kameraliste verschwunden ist.

    Edit: Du solltest dort nach meiner Meinung auch nix auf nullptr setzen, sondern das Element komplett rauslöschen.

    Am meisten Sinn würde für mich eine unordered map mit "ID" -> native_ptr machen.

    Edit2: Wenn ich so länger darüber nachdenke, solltest du auf jedenfall messen, was schneller ist. Ein vector mit bruteforce search, wäre im Hinblick auf Performance mit wenigen Elementen wahrscheinlich um ein vielfaches schneller.
    Auch wenn du in der Mitte löschen musst.
    Wenn du in deiner Kamera keine "Z-Order" brauchst (Reihenfolge) kannst du auch mit dem letzten Element swappen und dann poppen.


Anmelden zum Antworten