• Advertisement
Sign in to follow this  

When and where is it appropriate to use smart pointers in an engine?

This topic is 2173 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

I've been reading up a bit on smart pointers for a framework I'm slowly making, about the different types, advantages, disadvantages, etc. I haven't been able to find as much information or opinions on where it's appropriate to use in a game engine though. What is the general consensus about it? I'd like some specific examples please.

Share this post


Link to post
Share on other sites
Advertisement
I don't claim that this is any type of consensus, but I normally only use shared_ptr when I use polymorphism (which is very rare). I write a factory function that returns a smart_ptr<Base> so that I can do things like adding the object to a container of shared_ptr<Base>.

More complicated uses might be an indication that the ownership in your design is not clear enough.

Share this post


Link to post
Share on other sites
The general consensus is to use them where it makes your life easier and not where they don't.

You can certainly use them internal to the engine. It's preferable to avoid using them in the user API, but in some cases like resources where you want to prevent the user from deleting your references out from under you you've got little choice.

And the standard 'some console/embedded environments don't support C++ fully/well' also applies here. If your engine is supported on a few platforms, it needs to work well on those platforms. Smart pointer support is one of those things that I'd double check.

Share this post


Link to post
Share on other sites
I really don't think there are any "special" considerations that are due to a game engine here -- Yes, there are always concerns about compiler support and performance, but the former is due diligence (will this work on all my platforms), and presuming that smart-pointers will negatively impact the latter without evidence is premature optimization.

Smart-pointers solve a problem -- they make your life easier when and where you experience that same problem. That's (usually) the end of the story -- or rather, it *is* the end of the story unless something falls apart as a result. When you're eating a meal at home, on a train, or on a plane, you reach for a the same utensil regardless of where you find yourself. Most times, its the problem that begs the solution, rather than the environment -- so its a good rule of thumb that the first-pass solution should be chosen in that light unless its *already known* that the environment bares special considerations -- which is relatively rare.

In summary, use it where it solves your problems, then re-factor if and when that choice causes problems.

Share this post


Link to post
Share on other sites
Firstly, to answer your question, use them where they need to be used. My model shader manager distributes shaders to models upon request. When no more models are using those shaders, they can be deleted at the next state change (and any lingering shaders are reported as leaks, which luckily never happens).
When the user of my engine wants a pointer to a model, a shared pointer makes sure the model will not be deleted while still in use by either the user or the scene manager.

The rules for when to use them are basically governed by simple logic. If multiple unrelated systems need access to the same object, it is probably a good candidate for shared pointers.


In addressing some of the above issues:
#1: Boost is often not a good thing to include in real game projects.
#2: And that is partly because of compiler compliance and templates. Codewarrior is not the most template-compliant compiler on earth, and recently in-house I had to modify a game-team’s project to work with our in-house iOS engine using Xcode and GCC/Apple LLVM. A week of my life gone to fixing template errors.
#3: And how many compilers for consoles support C++11? Basically, the more you try to do with templates, the fewer platforms you can support.


But my engine uses shared pointers and has none of the above problems. Why?
Because I wrote my own. I checked out how Boost does it for a few minutes. As far as I can see there is a binary (or something) search happening every time you copy the pointer.
I store the reference count with the actual data being created, so copies are fairly instantaneous, and the implementation is simple enough not to cause problems with any relevant compilers, so it works fine on all of today’s platforms.

It is fairly trivial to write a shared pointer class so I would suggest you go that route if you have any of the above concerns.


L. Spiro

Share this post


Link to post
Share on other sites

Because I wrote my own. I checked out how Boost does it for a few minutes. As far as I can see there is a binary (or something) search happening every time you copy the pointer.

What what? You must have been reading some other piece of code.

I store the reference count with the actual data being created, so copies are fairly instantaneous, and the implementation is simple enough not to cause problems with any relevant compilers, so it works fine on all of today’s platforms.[/quote]
That's what intrusive_ptr does.

Share this post


Link to post
Share on other sites
In general, the rules for use of a smart pointer in a game engine are no different than for any other software. Which kinds of smart pointers you use may change a bit, however.

For example, in my engine and a dozen other AAA engines I've worked with, smart pointers to game objects acted a lot like std::weak_ptr. The handles were implemented quite a bit differently (generally via a slot map, but sometimes as object ids and a map of some kind), but the usage is similar. You can call a ptr() method which either returns a pointer to the object or nullptr, depending on whether the object is still alive.

For game resources (textures, shaders, sounds, etc.) it can be handy to use a similar system. However, I find it is even easier, safer, and more efficient to use a package system. A particular set of assets all reside in a package, which depends on other packages. Loading a new level will open up any packages the new level requires and unload the packed that it does not (also taking into account the UI packages and such). You can then safely use raw pointers to resources will no worry that the resource will disappear while the logic using the resource is still be executed. You can combine this with smart pointers for extra checking, potentially making those smart pointers just thin wrappers around pointers in a release mode build.

For a lot of the miscellaneous stuff, again, the rules are the same for games and other software. Prefer smart pointers for any "owning" pointer, with std::unique_ptr being your primary smart pointer.

Only use std::shared_ptr where you truly need shared ownership. Temporary pointers that will outlast owning handles can be raw pointers. Never create any smart pointer from a raw pointer without utmost care (too easy to end up with two different owners of a resource that will both try to free it).

Share this post


Link to post
Share on other sites

#2: And that is partly because of compiler compliance and templates. Codewarrior is not the most template-compliant compiler on earth, and recently in-house I had to modify a game-team’s project to work with our in-house iOS engine using Xcode and GCC/Apple LLVM. A week of my life gone to fixing template errors.

A week of your life gone to paying off part of the technical debt caused by working with a bad compiler. Now that you have upgraded to GCC, don't templates work better?

Share this post


Link to post
Share on other sites
I haven't been able to find as much information or opinions on where it's appropriate to use in a game engine though. What is the general consensus about it? I'd like some specific examples please.
When an object can have more than one 'owner' and has an indeterminable lifetime.
i.e. when you must rely on the features provided by the 'smart' pointer (ref-counting/GC) in order to determine the lifetime of the object.

[edit] also: when you're writing single-threaded code and/or are aware of the effects that multi-threaded reference counting has -- e.g. [font=courier new,courier,monospace]boost::shared_ptr[/font] is described as "thread safe", but still has performance pitfalls and easily created race conditions.[/edit]

e.g. if any kind of monster can pop up at any time, and each monster type needs to load a particular model (asset), then they could contain smart pointers to a ModelAsset object. All monsters of the same type will share that asset, and only when the last monster of a specific type is deleted will the corresponding asset be freed.

However, the above is an overly general-purpose implementation for most games.
It's more likely that your game is divided into specific 'levels', or 'zones' of an open-world type map, and that each level/zone is known to contains specific types of monsters. In this case, you can load all the models required for a zone up-front, and assign them all the same life-time as the zone (i.e. the zone is their owner). The monster instances also have their lifetimes bound to the zone, so can't possibly end up with dangling pointers.

In engine-level work, I'd default to the second case because it has more well defined behaviour. When doing systems level programming in C++, I'm a big fan of well defined lifetimes that you can easily reason about, so I've lost a lot of interest in smart pointers as a default option.
For higher-level gameplay code, the discussion is moot because when writing in a higher-level language like Lua or C# everything is a smart pointer anyway!

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement