Shared_ptr swap, and more


 
One of the benefits of working at a big technology company is the ability to attend talks presented by smart people. Yesterday I went to hear Stephan T. Lavavej, aptly known within the halls of Microsoft as STL. STL works on Visual Studio C++ libraries, including the C++ Standard Template Libraries (STL). His talk was on one of my favorite TR1 topics, shared_ptr.
 
I’ve been using shared_ptr for a while, and have spent some time understanding Boost’s implementation, so most of the presentation was just a refreshing review. However, some things were quite illuminating. I’ll talk about two illuminations, one technical and one philosophical.
 
The technical item I learned was about swapping shared_ptr. Consider a canonical swap of shared_ptr:
shared_ptr<int> a( new int(1) ); // suppose we have two shared pointers
shared_ptr<int> b( new int(2) );
 
{ // now we swap them
   shared_ptr<int> temp = a; // refcount of int(1) ++
   a = b;                    // refcount of int(1) –, refcount of int(2) ++
   b = temp;                 // refcount of int(2) –, refcount of int(1) ++
}                            // refcount of int(1) —
In the course of performing the swap, reference counts are modified six times. Because shared_ptr is threadsafe, refcount operations must be performed atomically, probably using some form of interlocked operations, which adds to the expense. Additionally, most (if not all) shared_ptr implementations store their refcounts in some area internally allocated by shared_ptr, so accessing the reference counts themselves required dereferencing pointers — six times. All we wanted to accomplish is swap two pointers, but there was a lot of needless overhead. The solution? Use shared_ptr::swap() instead:
a.swap( b ); // at least as fast as above, and considerably faster if you have a decent TR1 implementation
In VC9 TR1, shared_ptr::swap() does just what it needs to do and no more. The refcounts are never touched. Only the pointers are swapped. The VC9 TR1 version of free swap (e.g. swap( a, b )) is similarly optimized. Sweet.
 
The philosophical item I absorbed was just why shared_ptr completes the resource management picture for C++. Consider what we have without TR1. We have destructors, a deterministic resource release mechanism. We have: scope lifetime ("between the braces"), data member lifetime and container element lifetime, all of which are deterministic. And we have dynamic resources, which do not have a deterministic lifetime, but depend on us programmers to get it right. Therein lies the fatal flaw. Avoiding dynamic resource leaks, double deletions and so forth is Really Hard. That’s the problem shared_ptr solves, and it solves it extremely well. In the face of exceptions, in the face of early exit from functions, and even in the face of code that is modified over time, shared_ptr Just Works. I used to think of shared_ptrs as just a good way to copy around heavy-weight objects. Now I think of shared_ptrs as the default owner for any dynamic resource.
 
This entry was posted in C++. Bookmark the permalink.

4 Responses to Shared_ptr swap, and more

  1. I continue to move back in time and read your blog (still two years to read).

    I understand that this piece was written a few years ago, and you may have changed your mind with respect to smart pointers. If not, please forgive me, as I’m going to try to make you change your mind 🙂

    Smart pointers (shared_ptr-alike objects) are a bad code-related response to an very severe design-related issue. By manipulating them, you tend to beleive that you are manipulating the underlying object lifetime. It’s not true – at all. What you are manipulating in reality is the object ownership. In fact, smart pointers try to answer the question “who owns this object” with the questionnable “who cares?” answer. And this is not a good answer at all.

    If you don’t know who owns an object,

    * you don’t know who should control it, meaning that control of the object is spanned over numerous place, and this makes code maintenance more difficult (soon, it’ll be easier to implement a quick hack to get the work done instead of doing the right thing at the right place ; and you know where you go when you add more and more quick hacks :))

    * you don’t know when it will be released – and you have no control over this, meaning that at some point, you might face resource-related issues

    Smart pointers appear to give you a useful help – as a programmer. But then, it soon appears that they tend to creep all over the place. Soon enough, all objects are put inside smart pointers objects, and you non longer have any control on what happens. Strange corner cases arise, and you can’t get rid of them without creating another corner case somewhere else.

    If you design your software by making the strict assumption that at some point, any object is own by at most one object instance, then you get rid of all smart pointers – because suddenly, every object lifetime now has a definite scope. Can we make the assumption that an object can be owned by more than one object at a time? What does that really mean? I tried to bend my mind over this question for a few month, and I failed to get a useful answer. I cannot imagine how having shared property on some particular resource can be useful in any way. Even DirectX, a proeminent example of the reference-counted COM API, assumes that you own the objects you created (the reset mechanism in fullscreen in DX9 mandates this, as you must be able to rebuild all the needed resources when you swap to another application or minimize the window ; if you don’t – i.e. if you release your resources right after you set them in the device, you are at risk of staring a black screen sooner than what you expect. In my experience, no game does that, as you don’t even what resource will go AWOL).

    To conclude this answer, I’ll say that smart pointers are a lazy programmer trick (don’t be fooled by what I say: I tend to believe that lazy programmers are better programmers as they tend to find the most efficient way to get stuff done. Doing so requires imagination and knowledge of the existing techniques). But lazy programmers trick are often not design-resistant, unfortunately. And design is what, utimately, makes your code cleaner.

    Best regards,

    — Emmanuel

  2. Bret Kuhns says:

    I stumbled on this entry and found it informative, thank you. Then I read Emmanuel’s comment and I can’t help but wholeheartedly disagree with his conclusion. He makes a great point that smart pointers aren’t about lifetime, they’re about ownership. Then he fumbles around claiming smart pointers are inevitably a bad design choice. What’s ironic is that his very argument seems to lead toward unique_ptr, yet he never brings it up. C++11 introduces unique_ptr, and should be the go-to way to handle an owned object (as apposed to a raw pointer). Emmanuel’s own argument (excluding his conclusion) even seems to agree.

    Why is unique_ptr so great? Let’s say you create a new object on the heap and hold onto it with a raw pointer. Now you do something else on the very next line that throws an exception. What happens? Unless you’re wrapped in a try-catch block and explicitly delete on a catch, then you get a memory leak. But okay, I just explained how to take care of it, so what’s the problem? Well, are you going to trust yourself, or another developer five years from now updating the code, to ensure at each and every point that you’re exception safe and that your catch blocks properly manage resources? Good luck with that.

    Instead, wrap the owned object with a unique_ptr. Now if that exception gets thrown on the very next line, the unique_ptr is destructed and automatically frees the memory. Nice!

    Now, back to Emmanuel: “If you design your software by making the strict assumption that at some point, any object is own by at most one object instance, then you get rid of all smart pointers”. We all know what happens when you make assumptions. The unique_ptr was designed explicitly to prevent copying, keeping the instance of the object it wraps unique, thus its name. The uniqueness of that object is no longer an assumption, it’s enforced. I’ll trust the compiler over myself or another developer any day.

    Overhead on a unique_ptr is also nearly negligible compared to a raw pointer. Meanwhile, it provides automatic memory management and ownership semantics far richer than a raw pointer.

    So back to my original argument, unique_ptr should be your default choice when you’re managing an owned resource. If “client” code needs to grab onto that object and extend it’s lifetime, then you’re in shared_ptr territory. Luckily the unique_ptr -> shared_ptr conversion is very simple. If a piece of code doesn’t intend to own the object, then accept a const reference and ignore smart pointers. They’re for managing ownership, so the semantics of the code should reflect that.

  3. @Bret Kuhns
    I agree all the long (my point was not really about C++, but about designing with C++ in mind). I have not cited unique_ptr because there is no difference between a raw pointer and a unique_ptr with respect to the designer point of view. Code wise, unique_ptr enforce the assumption made while designing the application, which is a very good thing. But the way unique_ptr works also means that it’s not a smart pointer object (despite the fact that it appears in the so-called smart pointer library). It’s a wrapper – a very good wrapper, far better than auto_ptr – which offer the same base services (modulo the known limitations of auto_ptr which are more language limitations, as it tried to implement move semantics without syntaxic support for move semantics). Calling it a smart pointer is an exageration of the reality (ok, it’s “smarter” than a raw pointer, but who isn’t?).

    Thus my rant was not about saying that unique_ptr was a no go, but that real smart pointers as we mean it when we speack about smart pointers (i.e. those with shared ownership semantics) are dangerous to use in code. The software design shall acknowledge that and shall avoid shared ownership semantics when possible (in turns, that makes shared_ptr a much more dangerous class than auto_ptr IMHO ; auto_ptr shortcomings are tied to its code, while shared_ptr shortcomings are tied to its very existence).

    Best regards,

    — Emmanuel

  4. Bret Kuhns says:

    @Emmanuel Deloget
    I certainly can’t argue with your reply. I made the mistake of implementing a section of a project using shared_ptr (after having started with unique_ptr), thinking I couldn’t do it with move semantics. I’ve since realized I was wrong and now the design will make it more difficult to undo what was done. Using something like shared_ptr should certainly be evaluated closely to be absolutely certain it’s necessary. Either use move semantics outright, or the unique_ptr “RAII wrapper”.

Leave a reply to Bret Kuhns Cancel reply