
Don G wrote:
Hi Peter,
Sorry it took so long for me to respond (real life just keeps getting in the way), and thanks for taking the time to reply.
shared_ptr only contains the features that are absolutely essential for the answer of "is shared_ptr suitable" to be "yes" in the vast majority of cases (and to stay "yes" as the design evolves).
I guess this is the crux of the my contention. I think all must agree there must be: automatic counting; basic pointer syntax (including any implicit conversion from derived to base, and various operators). The features beyond those are of the debatable variety.
There are two features beyond what you listed: 1. "Do the right thing" destruction; 2. Weak pointer support. Note that (1) does not equal "custom deleter support". Custom deleters do not add any further overhead (when not used) over "do the right thing" destruction. By that I mean that shared_ptr<X> can always be destroyed when it has been constructed properly, no matter whether at the point of destruction X is a complete type with an accessible (and virtual, if the actual object is not of type X) destructor, or whether 'operator delete' at the point of destruction can deallocate from the 'operator new' heap at the point of construction.
... so this brings down the design-imposed overhead to four words per object for an empty deleter.
From 1.32 code, shared_ptr never has an empty deleter unless it is itself empty (or NULL). Unless I missed something? If my analysis is correct, an overhead of 4 words is required for the deleter alone:
- shared_count must contain a pointer to the sp_counted_base - sp_counted_base has a vtable - sp_counted_base has a pointer - sp_counted_base has a functor
The weak_ptr adds an extra count (1 word) but more importantly requires a mutex, at least on platforms lacking more advanced lock-free primitives such as InterlockedCompareExchange.
The theoretical minimum cost of a non-intrusive pointer is: - per-instance pointer to the count; - one word for the count. Feature (1) above adds: - vtable - pointer The functor need not be present when not used, although the current code base does not do that. It can also be optimized out when it's an empty class with the help of boost::compressed_pair. Again, the current code base doesn't do it, but we're talking about the design-imposed overhead here. Feature (2) adds - an extra count. For platforms lacking CAS (are there any?) Tyson Whitehead has posted an implementation: http://lists.boost.org/MailArchives/boost/msg66868.php that locks a mutex only when weak_ptr::lock is used. If weak pointers aren't used, the implementation has essentially the same performance as an ordinary non-intrusive smart pointer. The mutex pool trick can be used to not include a per-count mutex.
I suspect that you haven't had much experience with the design possibilities opened by weak pointers or custom deleters ...
I have not; shared_ptr et.al. is new to me. My own work never followed those paths for two reasons. The custom deleter never occured to me because I got the same effect with intrusion.
One situation where custom deleters come in handy is when you want to use your own smart pointer in an application, but a third-party library takes a shared_ptr. With a shared_ptr not supporting deleters, you'd be forced to use shared_ptr in your application, too.
The weak_ptr role was filled by T* or T&, and the standard refrain of "be careful what you are doing if you go this way." Of course, in most cases one can do some refactoring and remove the need for the weak reference.
You can always switch to active notification and avoid the need for a passive observer such as weak_ptr, but this often makes your design worse, because now the observed objects need to know about the observers in order to notify them. There's also the 'shared from this' problem. [...]
The good thing is that once you have a stable design, efficiency usually follows, as happened here.
True enough, but optimizations leveraging advanced, platform-specific capabilities do not offer much benefit to those writing portable code. For them, the cost remains the same. In this case, this will always be so.
Portable code? shared_ptr doesn't need to be portable, if it comes with your standard library.
Now we've come back to my original point: the very design of shared_ptr requires a certain overhead that is _irreducible_ in the general case. And that overhead is:
A. Not clearly specified or articulated (for those who need to know) B. _Much_ more than one would naively expect
All designs require a certain irreducible overhead, do they not? Think of it as a std::map. The overhead of a map is not clearly specified or articulated. (B) probably applies as well. But it works.