Hey folks, today’s article will deal with smart pointers. First of all, I should mention that I am a fan of smart pointers in general but also, that I dislike some of the design decisions which were made on them (particular in the weak_pointer design).
For those who don’t know smart pointers, here is a short intro:
We got three different smart pointers in the C++ standard. The unique_ptr, the shared_ptr and the weak_ptr. The convenient thing with smart pointers is the fact that they will (usually) prevent you from creating memory leaks since they offer small garbage collection like behavior. The different pointers have different approaches for this which also comes in very handy to display the ownership for your objects.
The unique_ptr has the exclusive ownership for an object. That means if the lifetime of the pointer expires it will free the memory of the object it is pointing to. There can only be one unique_ptr pointing on the same object.
The shared_ptr has a shared ownership for its memory, which means it will free the memory if no other shared_ptr is pointing to the memory anymore. This also means, that there can be more than one shared_ptr pointing to the same memory.
The weak_ptr has a weak ownership for a memory segment which is managed by a shared_ptr. The weak_ptr has no influence on the lifetime of a chunk of memory. If all the shared_ptr are expired, the weak_ptr will get a “signal” which invalidates the pointer. weak_ptr are meant to access objects without obtaining ownership of them, but you can only access the object by converting the weak_ptr into a shared_ptr.
Okay! Sounds awesome, right? Usually smart pointers are a good choice indeed, but there are also situations where smart pointer are a real pain in the ass.
First example: Static smart pointer
Let’s consider you are writing an engine based on an actor model just like Unreal Engine. Usually the engine owns the actors and gives you a pointer to an actor if you need one.
Now let’s consider we would try to use smart pointers for our engine. The code might look like this (only the parts of interest):
Pretty basic. We got a map to store our actors (usually a flat_map instead of a real one but hey, it’s std::) and a findActor(name) function to find a special actor. We use shared_ptr since we couldn’t construct a weak_ptr from a unique_ptr but on the other hand we don’t want to give the ownership of our actor to the outside world which means we need to use a shared_ptr in combination with weak_ptrs.
And here is one of the first flaws. If we want to access an actor we got from the engine we would have to do something like this:
The lock() function gives us a shared_ptr which can access our actor. That means, each time we access our actor we will have to CONSTRUCT A SHARED_PTR!!!.
Constructing shared_ptr isn’t cheap. They have to create a whole lot more stuff than raw pointers, which also degrades the performance of the weak_ptr by a lot.
The example I used to test the performance difference is quite extreme but it will give you at least a relation how the numbers might differ. This is how I tested:
SomeStruct.add(xyz) simply adds integer values to a n * 3 reserved vector<int> to pretend some work, delta timer is a simple timer object for benchmarking and stuff and mango::string32 is a simple in place string.
The code above under Windows 7 with a core I5 4570 produces the following numbers:
If you increase n, the relation between the number stay more or less equal. The first value is our raw pointer access performance and the second one is our weak_ptr access performance. (with the fact in mind that vector::push_pack isn’t for free). The weak_ptr.lock()-> access takes literally 4-6x the time, raw pointer-> does.
This is a whole lot if you ask me. Where does this difference come from? Well, the regular shared_ptr-> is as fast as raw pointer-> but the construction of the shared_ptr is as I already mentioned not cheap and this makes the weak_ptr in my opinion a poor choice for this example. Accessing member functions of game objects and actors will happen a whole lot during the runtime of our game and giving away that much performance seems like a bad idea.
Ok, now I said that shared_ptr have the same performance as raw_ptr if it comes to access what happens if we change our Game::findActor() to shared_ptr? Said and done:
And our test:
Ok, our shared_ptr has the same performance, problem solved…. kind of. I am not particular happy with our shared_ptr solution, but why? Well, it’s easy. Let’s consider the developer is too lazy to request the same actor for the same function over and over again and comes to the decision that a static shared_ptr is his solution of choice. This will lead to a sure memory leak if the developer can’t pull a really good trick out of his hat to notify that the engine abandoned the shared_ptr on the actor he is accessing. Now we got a shared_ptr which will exist until the end of our program and the memory might never be released since the developer would have to reset the pointer but the actor won’t be updated or drawn anymore since the engine no longer has access to it. Well that’s a big problem. With one actor the problem might be negligible but with more actors it will become a big issue.
Now one could come to the genius idea to hold a shared/unique_ptr to the actor and use a raw pointer to hand it outside. This will work, but it is a really ugly solution and I am no fan of mixing raw and smart pointer.
A good solution would be to use raw pointer and overwriting the operator new/delete to prevent the developer from deleting your actor from the outside while freeing the resources on the inside of your engine. This might be annoying since raw pointer aren’t modern C++, but sometimes you might get yourself into situations where you have to fallback to less elegant but more steady solutions.
There are cases in gaming where smart_ptr are a good solution in game development (managing internal resources like controllers for instance) but if you are handing objects from your engine to the outside world, you should really ask yourself if smart pointer are the right way to do so, since the weak_ptr have a really bad performance and shared_ptr will break your ownership model.
That does not mean you should avoid smart_ptr at all, but you should at least think of the possible flaws that are related to them. Overall I think they usually are a way better solution than managing memory lifetimes by hand, but I also think they are overrated by many people.
They aren’t neither garbage nor the holy grail of C++ pointer management.