So far we’ve covered Values, references, bare pointers, and
unique_ptr in my more info then you ever wanted about which reference to use when series. Today we’ll move on to
unique_ptr’s name implies there can be only one
unique_ptr pointing at a given object (unless you’re pulling some sort of shenanigans that will get the the undefined behavior that you deserve). If you need a smart pointer that doesn’t have this restriction then you’ll want to look at
shared_ptr with which you can have an arbitrary number of pointers pointing at a given object. They even keep track of themselves and when the last one stops pointing at the object the object gets destroyed. It works great but there is a cost as we’ll see below.
Given the name of
shared_ptr it should come as little surprise that if you return a
shared_ptr then you’re offering to share ownership of the object with the caller. This sounds great and you might be wondering why we don’t just use
shared_ptr all the time. Well, there’s some catches to
shared_ptr. First off, they’re a bit inflexible about ownership in that you can only share ownership with other
shared_ptr objects; once something goes into a
shared_ptr you can only ever store it in another
shared_ptr. Copying a
shared_ptr is also slower than copying a bare pointer or
unique_ptr due to having to increment the reference count and – unless you’re running a single threaded program – that increment involves thread synchronization. Its a quick
std::atomic style increment, but its still more overhead than the alternatives. They also take up more space than a
unique_ptr since you need a separate reference count object and each
shared_ptr instance needs a pointer to the reference count object. That being said you’ll probably end up using them a lot as they are extremely useful, just be aware of what you’re signing up for.
unique_ptr, you should only take a
shared_ptr as a function argument if you intend to take shared ownership of an object. If you don’t want to take ownership then you should be taking a reference or bare pointer. When you take a
shared_ptr as an argument normally it should be done by value and then you should move the object into the
shared_ptr that it is ending up in (remember: you’re only taking a
shared_ptr argument if you want shared ownership which means you must be storing the object in a
shared_ptr). That will allow the caller to move the object into the argument if possible getting rid of any
shared_ptr copies and the reference count gymnastics that go along with those copies. If for some reason you won’t be able to move the object into the final
shared_ptr then take the
shared_ptr argument as a
const reference instead in order to avoid making extra copies. The name of the game is avoiding unnecessary copies and the accompanying reference count changes.
shared_ptr for member variables when you are sharing ownership of an object, it’s that simple. As mentioned above,
shared_ptr does take up more space and cost more to copy than a
unique_ptr so if you don’t need shared ownership then
unique_ptr is the better option.
shared_ptr is pretty straight-forward: the reference count is normally handled with a
std::atomic (or some other integral type) so you don’t need to worry about its thread-safety. What you do need to watch out for is using the same
shared_ptr instance in more than one thread simultaneously. If you aren’t synchronizing access to the single instance of
shared_ptr then if accesses on two threads interleave just right you can end up in a situation where one thread has decremented the reference count reaching zero while another thread has incremented the reference count ending up with one. The reference count will work out fine since it’s handled with atomics, but now there’s one thread that thinks the reference count is zero and is going to delete the object while the other thread is blissfully using that same object since it thinks the reference count is one. Note that there’s no way for the thread that sees reference count one to know if it got there by incrementing the reference count to two and having the other thread decrement the reference count to one, or if things happen in the opposite order (first the count goes to zero before going back to one). The only way to fix this would be to use a mutex instead of atomics, holding a lock on the mutex while deleting the object after decrementing the reference count. But this would increase the overhead of
shared_ptr, and many already balk at the existing overhead, so instead it’s up to us to protect our
shared_ptrs when necessary.
The final smart pointer, one intimately related to
weak_ptr – about which I’ll have lots to say next time.