I'm making a basic platformer game. I have a Game class as well as Level class. The game object holds a pointer to the current Level object. A level currently has a std::vector of GameObject raw pointers, and normally it handles the destruction of the GameObjects. However, some of them are Player objects (a derived class from GameObject). These should not be deleted when the level is, as they are owned by the Game.
My current Level destructor looks like this:
Level::~Level() {
for (GameObject* obj : gameObjects) {
if (!obj->livesPastLevel()) {
delete obj;
}
}
}
I want to convert from raw pointers to smart pointers. Changing the Game's pointer to a level into a unique_ptr is easy. However, I'm having trouble thinking of a good way to handle the Level's gameObject* vector.
I can't just use unique_ptrs because then the Player would get deleted when the Level is destroyed. I know the normal solution would just be to use a shared_ptr, but these seems like overkill. The majority of the pointers will just have one owner (the level) and it would be needlessly slow to reference count those with shared_ptr.
Is there any good way to use a vector<unique_ptr> except a few of them are actually owned by a different object? Like setting unique_ptr.autoCleanup=false. Or is there another pointer type I should use to handle that?
Or perhaps the overhead of shared_ptr is just something I have to deal with. It's also possible that I am structuring this whole resource management incorrectly.
So how can I use smart pointers when most (but not all) would be usable as unique_ptr?
1 Answer 1
You have a situation with shared ownership of objects. It is therefore frustrating to see you dismiss std::shared_ptr which would address exactly this situation, and to try using std::unique_ptr which is designed to prevent shared ownership. Yes, there's some overhead from reference counting and you have to avoid cycles, but this is by far the easiest way to manage your game objects.
Since the shared-ptr refcounts only have to be updated when a level is initialized/deleted, and since that is quite rare in a typical game (not thousands of time per second), any time overhead is likely negligible.
But for completeness, yes, there are alternatives:
have separate vectors for owned vs shared game objects in your level. This has no refcouting overhead and therefore seems quite attractive, but iterating through all game objects in a level is now a bit more complicated.
in the level destructor, try to downcast (
dynamic_cast) the game object toPlayerand only delete them if this fails. Of course, this could feasibly be more expensive than refcouting. It is also a tremendously fragile approach. YourlivesPastLevel()method is a slightly more sane variant of the same idea.make it fine to delete the player's game object by separating the player's per-level state from the persistent state. This can be a very elegant approach, and the additional pointer indirection might not matter that much. Whether this is feasible depends on whether other game objects could persist beyond the level, e.g. items picked up by the player. If you don't know up front which data has to survive the level, shared ptrs will be much easier.
As a general point, consider that runtime performance is not the only requirement you have. Development costs, user experience, or security requirements might also affect your decisions. Whereas a shared_ptr makes it easier (cheaper) to create software that has no segfaults or use-after-frees, it does have a bit of runtime overhead. The question is: is this a good tradeoff? Is it better to have a simple design with a bit of overhead, or is it better to choose a more fragile low-overhead design that might lead to tricky bugs later? You're the developer, so ultimately you have to know what's better aligned with your priorities.
-
Thanks for this answer! That's a good point that level creation and destruction is rare, so the overhead will be very small. And shared pointers are probably already optimized a ton so it will barely matter.Luke B– Luke B2020年09月06日 16:30:31 +00:00Commented Sep 6, 2020 at 16:30
-
Nice overview of different strategies and tradeoffs.Deduplicator– Deduplicator2020年09月06日 17:05:27 +00:00Commented Sep 6, 2020 at 17:05
-
1@BakedPotato well, there's a bit of memory overhead from shared ptrs that I didn't go into. But that shouldn't matter in most cases. The important part is that you only store the game objects as shared ptrs, but your other code can use shared_ptr::get() and use them as raw pointers as before.amon– amon2020年09月06日 17:23:48 +00:00Commented Sep 6, 2020 at 17:23
-
Also w.r.t. overhead, if you create the pointer with
make_sharedthe heap/memory overhead is reduced too - both space and time.davidbak– davidbak2020年09月07日 01:09:16 +00:00Commented Sep 7, 2020 at 1:09
Explore related questions
See similar questions with these tags.
Playerobject (and with all the data inside, i.e. "deep copy"), then throw away the original instance. Whether this is "elegant" or "nice" depends on other details in your architecture.unique_ptritself when implementing your idea. Put theautoCleanupflag inside your user-defined type (GameObject), and then your actual cleanup code should read:vector<GameObject*> forKeeping; vector<GameObject*> forDiscarding; for (GameObject* obj : gameObjects) { if (obj->autoCleanup) forDiscarding.push_back(obj); else forKeeping.push_back(obj); }Instead of moving things into aforDiscarding, you can also delete them right away.shared_ptr" - frankly, whenever I read statements like this about hypthetical performance, chances are high they are unjustified, superstitious nonsense. Go, try it out, and when this simple solution does not match you performance requirements, then think about a more complex solution.