Smartpointer-Hype vs. Over-Dev



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



  • Ich finde Klassen werden aktuell echt gehypet. Die werden irgendwie überall zu viel benutzt. Ich habe mir da deshalb meine eigene.............



  • chp++ schrieb:

    oder an der Stelle an der entschieden wird ,dass das Objekt zerstört werden muss.

    Ich habe es jetzt an genau dieser Stelle eingebaut. Die Dungeons kennen die alle Kameras - im Moment habe ich im Splitscreen bis zu 4 Kameras, d.h. der Aufwand hält sich in Grenzen. Und die Kameras werden über das Verschwinden der Objekte informiert:

    void Camera::notify(DungeonEvent const & event) {
        if (event.type == DungeonEvent::Type::ObjectVanished) {
            if (focus == event.object) {
                focus = nullptr;
            }
        }
    }
    

    chp++ schrieb:

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

    Die Camera-Instanz besitzt ein Attribut GameObject* focus - das setze ich halt um wenn die Kamera einem anderen Objekt folgt

    chp++ schrieb:

    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.

    Den Ansatz der linearen Suche auf einem Vector werde ich mir für geeignete Stelle merken, Danke 🙂 !!

    @Archti: 😃



  • Marthog schrieb:

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

    bei mir leakt bei exceptions nichts, und ich muß momentan projektbedingt in "klassischem" C++ ohne smart pointers programmieren.

    Kennst Du eine Situation, in der memory leaks bei exceptions unabwendbar sind?



  • großbuchstaben schrieb:

    bei mir leakt bei exceptions nichts, und ich muß momentan projektbedingt in "klassischem" C++ ohne smart pointers programmieren.

    Gewagte Aussage. Die meisten die das behauptet haben haben immer noch doof geguckt.
    Klar, ein Programm dass im Standardfall nicht leakt ist auch ohne RAII keine Kunst. Ein Programm das in keinem denkbaren Sonderfall leakt ... ohne RAII, aber mit Exceptions ... ist schon ne Herausforderung.

    großbuchstaben schrieb:

    Kennst Du eine Situation, in der memory leaks bei exceptions unabwendbar sind?

    Unabwendbar sind sie nie.
    Bloss muss man verflixt aufpassen. ODER, was die schlauere Variante ist, sich seine eigenen RAII Hilfsklassen schreiben.

    Oft sieht man z.B. so Sachen wie

    Thing t = AllocateThing();
    DoStuffThatCannotThrow(t, somePath + "\\file.ext"); // operator +(std::string const&, char const*) => std::bad_alloc => Leak
    ReleaseThing(t);
    


  • großbuchstaben schrieb:

    Marthog schrieb:

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

    bei mir leakt bei exceptions nichts, und ich muß momentan projektbedingt in "klassischem" C++ ohne smart pointers programmieren.

    Kennst Du eine Situation, in der memory leaks bei exceptions unabwendbar sind?

    Zeig mal, wie du ein Array von Strings via new allozierst. Exceptionsafe. Ohne Leak.



  • @Nathan
    Ich verstehe grad das Problem nicht.
    Also wo da eine Exception fliegen soll, und wo dann was leaken soll. Und was genau du mit "Array von Strings" meinst.

    Alles was mir an Möglichkeiten einfällt kann man, wenn man dran denkt, mit catch (...) { do_the_cleanup(); throw; } abdecken.
    Ist bäh, aber funktioniert.

    Oder steh' ich grad auf der Leitung?



  • Nathan schrieb:

    großbuchstaben schrieb:

    Marthog schrieb:

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

    bei mir leakt bei exceptions nichts, und ich muß momentan projektbedingt in "klassischem" C++ ohne smart pointers programmieren.

    Kennst Du eine Situation, in der memory leaks bei exceptions unabwendbar sind?

    Zeig mal, wie du ein Array von Strings via new allozierst. Exceptionsafe. Ohne Leak.

    ich würde std::vectorstd::string nehmen, und new vermeiden, stattdessen automatischen Destruktor-Aufruf beim Verlassen des scopes ausnutzen. Wieso sollte bei new/delete mit exceptions zwangsläufig ein leak entstehen?



  • Hi, inzwischen habe ich eine eigene Implementierung auf Basis von std::unique_ptr gebaut: sole_ptr (owning) und sein "observing" Gegenstück dynamic_ptr (non-owning). Hier die grobe Idee:

    • dynamic_ptr besitzen einen (nativen) Zeiger (nennen wir ihn parent ) auf einen sole_ptr
    • sie "melden" sich bei diesem "an", d.h. ein sole_ptr kennt alle dynamic_ptr die sich angemeldet haben
    • Gibt der sole_ptr sein gehaltenes Objekt frei, werden alle dynamic_ptr informiert und ihre parent Zeiger auf nullptr gesetzt. Ok, das ginge auch mit einem Customized Deleter ^^
    • Wird ein sole_ptr verschoben (hier wird es interessant), informiert er auch alle dynamic_ptr darüber und sagt ihnen, welcher sole_ptr nun die bisherige Ressource verwaltet. Dadurch werden dynamic_ptr nach einem std::move auf einem scope_ptr nicht ungültig (oder zeigen auf einen leeren smartpointer).
    • Ob ein dynamic_ptr ungültig ist kann mit bool expired() const; abgefragt werden

    Haters gonna hate. Alle anderen finden den Code auf GitHub:

    konstruktives Feedback erwünscht 😃

    Testcode in a nutshell:

    // [...] Rest siehe Testcase-Code
    
    struct Foo {
        int id;
        std::string name;
    
        Foo(int id, std::string const & name)
            : id{id}
            , name{name} {
            std::cout << "+" << this << "\n";
        }
        virtual ~Foo() {
            std::cout << "-" << this << "\n";
        }
    };
    
    int main() {
        // create sole ownership
        auto owner = make_sole<Foo>(42, "Anonymous");
    
        // create non-owning pointer
        dynamic_ptr<Foo> user{owner};
    
        // operate on `user`
        user->id++;
        user->name += std::to_string(user->id);
        if (!user.expired()) {
            std::cout << "Foo[" << user->id << "," << user->name << "]\n";
        } else {
            // this cannot happen in this case, because user didn't expire, yet
            std::cout << "Foo expired (unexpected!!)\n";
            return 1;
        }
    
        // move ownership
        auto other = std::move(owner);
        std::cout << "After moving ownership, non-owning expired() is " << user.expired() << "\n";
    
        // end ownership
        other = nullptr;
        std::cout << "After ownership ended, non-owning expired() is " << user.expired() << "\n";
    
    }
    

    LG Glocke



  • Glocke schrieb:

    Haters gonna hate.

    Also den Spruch, und die dem Spruch zu Grund liegende Einstellung, finde ich reichlich doof.

    Davon abgesehen... was du da zusammengebastelt hast macht mMn. schon Sinn.
    Threadsafe ist es halt nicht, bzw. es ist nicht ohne signifikanten Aufwand threadsafe zu bekommen. Was aber für viele Anwendungen egal ist.



  • hustbaer schrieb:

    Also den Spruch, und die dem Spruch zu Grund liegende Einstellung, finde ich reichlich doof.

    😃 Wer an dieser Stelle lieber shared_ptr nimmt, kann das machen und meine Lösung als Unfug abtun .. alle anderen dürfen über den Spruch schmunzeln und müssen keine tiefere Bedeutung hineininterpretiere 😉

    hustbaer schrieb:

    Davon abgesehen... was du da zusammengebastelt hast macht mMn. schon Sinn.
    Threadsafe ist es halt nicht, bzw. es ist nicht ohne signifikanten Aufwand threadsafe zu bekommen. Was aber für viele Anwendungen egal ist.

    Thread- und Exception-safety stehen schon auf der Todo-Liste 🙂



  • Ähm.
    Wenn du es threadsafe machen willst, dann kannst du gleich genau so gut intern shared_ptr und weak_ptr verwenden. Und dir damit nen Haufen Arbeit sparen.
    Weil es dann genau so langsam wird.

    Bzw. ... genaugenommen kannst du es gar nicht threadsafe machen ohne weitere Umbauten.
    Weil du, wenn es threadsafe sein soll, die weak_ptr::lock Semantik brauchst.
    Sonst könnte dir ja ein anderer Thread den "sole_ptr" resetten während dein Thread gerade auf das Objekt zugreift.

    Also... wenn du eine Lösung anzielst die threadsafe ist... dann würde ich wirklich sagen: das ist Unsinn.



  • Und wieso virtuelle Destruktoren?



  • Nathan schrieb:

    Und wieso virtuelle Destruktoren?

    Gewohnheit :S

    @hustbaer: schau ich mir mal genauer an


Anmelden zum Antworten