POINTING THE WAY REDUX, PART 5: SHARED_PTR

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 shared_ptr. As 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.

As with 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.

You’ll use 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.

Thread-safety with 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 shared_ptr, is weak_ptr – about which I’ll have lots to say next time.

Advertisements

Share this:

Like this:

Related

3 comments

[…] Direct access to an object stored in a unique_ptr member variable – should you decide to provide it and you proabably shouldn’t – should be done by returning references or bare pointers to the object, not by returning references to the unique_ptr itself. That would reveal implementation details unescessarily and confuse the caller about what was going on with the object’s ownership. Should you decide to provide direct access to the object via bare pointers or references then, as explained above, you have onerous thread-safety restrictions on how you use the unique_ptr. Specifically: you can never change what the unique_ptr points at. Most of the time the better strategy is to not expose the object directly and instead provide methods in the object that contains the unique_ptr that use the pointed to object. By doing this you can synchronize everything so that should you need to change what the unique_ptr points at you can safely do so. If you do need to provide direct access to the pointed to object then most likely your member variable should be a shared_ptr instead, about which I’ll have more to say in a future post (should be the next one). […]

[…] shared_ptr was the latest installment in my more info then you ever wanted about which reference to use when series that has also covered values, references, bare pointers, and unique_ptr. This time I’ll be covering the last of the smart pointer trifecta: weak_ptr. […]