Yes, that is true, if you can arrange it that way, e.g. in most level-based games. It may not be that easy, however. For example, in an open-world game, some rare unit comes along (say, a wandering boss creature, I seem to remember Ryzom had these). The only working solution to the "you know because of contract" type of ownership is to load all bosses (whether one walks past you or not, and regardless which one it is) and keep them all loaded. Because hey, you never know what happens in half an hour, maybe. But that may be undesirable. It may be more desirable to only load the boss that is showing, on the clients that can see it. And free the memory once it's dead.No, that's not how ownership semantics work.[...] In a level-based game, assets are loaded for a particular level, and the sound-player is an object within that level. The level's assets are only unloaded when the level is unloaded, at which point, all level-specific objects (such as sound-players within the level) will also have been destroyed.
Another example would be a game with voice acting where a NPC tells some long sad story about how his father was killed by wolves and how he lost his aunt's ring which you are to retrieve. This is something you want to stream in once, listen to, and never use again. You definitively don't want to keep that around forever or load it ahead of time "just because", especially since there are maybe 50 such NPCs in this city.
The nice thing about the shared pointer approach is that you simply don't care (and you don't even know how long that will be, nor do you have to care!). What's needed stays in RAM, what's not needed is kicked. Eventually.
That's wrong, or rather it is correct but inconsequential.The performance impact of shared_ptr isn't great in single-threaded code, but is downright stupid in multi-threaded situations. If you saw the actual behavior of that solution written out as plain old C code -- without the use of shared_ptr to hide its stupidity behind an abstraction -- you'd fire whoever wrote it... at least in the context of a game engine.
e.g. The hidden reference counters used internally by shared pointers are thread-safe (via atomic increments), but the shared pointers themselves are not, so they need to be externally synchronized by a mutex/etc...
The reference counter is updated with atomic increments/decrements, that's right. And this is a good thing most of the time (indeed always, except when you are strictly single-threaded, then it's wasting some cycles). The impact of this is however rather low. You do not load 250,000 assets every second. You do not copy around shared pointers 250,000 times per second. That's not what happens. For the comparatively low number of copies that you need to make (few hundred), the overhead of the atomic increments is very acceptable in comparison to the advantages that you buy with them. Yes, it's not free... but by no means a limiting factor unless you do very stupid things.
The pointer itself is, as you point out correctly, not thread-safe. But that is a good thing, not a bad thing. Not stupid, not in any way. You cannot create or reassign a shared pointer in a thread-safe manner. Yeah. Who cares! You don't want to do that anyway.
The shared pointers are created solely by the "manager". One thread, no concurrency. After that, they are, by all practical means, read-only objects (until destroyed). They are deleted solely by the last user decrementing the refcount (whoever that may be). Again, one thread, no concurrency anywhere (not anywhere where the fact that the pointer is not threadsafe would matter, anyway).
The weak pointers are admittedly upped concurrently (well, not necessarily, but at least possibly), but that's fine. The library guarantees that the refcount is managed correctly and once upping the weak pointer to a shared pointer has succeeded, the pointer is valid. Always, no exceptions. No mutex needed.
And if it didn't succeed, the pointer is invalid by definition, so you couldn't care less whether the value is garbled.
To use your wording: Shoot the guy who uses mutexes because they're not needed. :)
No. Every copy or upping a weak pointer (which is a kind-of copy) causes an atomic increment. Unless you do something very stupid and copy around the shared pointers all the time, the number of actual copies is comparatively low.every lookup causes an atomic increment, hopefully with relaxed memory ordering
On the other hand, you can look up (dereference) the smart pointer a million times without an atomic operation otherwise. Because yeah, it's not threadsafe. Luckily.
The overall cost of upping the pointer is very moderate (I intentionally avoid saying "locking" because it gives a wrong impression). Something like once per frame per asset. How many textures and meshes do you have? A hundred? So that's 200 atomic increments per frame. And...? The problem being?
Consider how many cycles it costs to look up an asset in a map, or find it with a linear search in a vector of objects -- nobody even bothers about that. Why not? Well because it happens relatively rarely.
Like I said in my first sentence: It's maybe not a perfect fit for everybody, but I think it is overall a valid approach.