2

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
    This question is better asked at Stack Overflow. – πάντα ῥεῖ Sep 6 '20 at 0:39
  • A very easy way is just make a copy of the Player object (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. – rwong Sep 6 '20 at 0:41
  • You don't have to modify unique_ptr itself when implementing your idea. Put the autoCleanup flag 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 a forDiscarding, you can also delete them right away. – rwong Sep 6 '20 at 0:45
  • 3
    " it would be needlessly slow to reference count those with 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. – Doc Brown Sep 6 '20 at 6:34
  • 2
    Apart from using smart pointers being a good idea in general: Throwing owning pointers and non-owning pointers into the same vector is a very bad idea. You will mess this up at some point. That’s where I would start. That vector should hold either players or other GameObjects, but not both. – besc Sep 6 '20 at 10:56
8

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 to Player and only delete them if this fails. Of course, this could feasibly be more expensive than refcouting. It is also a tremendously fragile approach. Your livesPastLevel() 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. – Baked Potato Sep 6 '20 at 16:30
  • Nice overview of different strategies and tradeoffs. – Deduplicator Sep 6 '20 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 Sep 6 '20 at 17:23
  • Also w.r.t. overhead, if you create the pointer with make_shared the heap/memory overhead is reduced too - both space and time. – davidbak Sep 7 '20 at 1:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.