Manual memory management in C++ can easily become a nightmare. To alleviate this problem, someone invented smart pointers. Today we’ll focus on reference-counted ones, or, more precisely, why non-intrusive boost::shared_ptr (also called std::tr1::shared_ptr, since it became a part of TR1) is often a worse idea than, as the name suggests, intrusive, intrusive_ptr (of course, using a garbage collected language is almost always better).
The first thing is the performance (you care about performance, don’t you? If you weren’t, you wouldn’t be using such an unsafe language, would you?). There are actually several performance-related problems. The first one is thread-safety. It’s not necessarily bad, if your code is actually multi-threaded. But even then, you’re paying the price for that everywhere, even in the places where you’re absolutely sure you don’t share objects across threads. And you can’t turn off thread-safety in such places – you can do that either for whole project, or not at all. Pick your poison.
The second performance-related problem is that shared_ptr has to allocate memory for the reference counter on the heap. Yes, it’s possible to avoid this by using make_shared or allocate_shared, but it’s actually an ugly hack. The memory footprint of this smart pointer is no picnic, either – shared_ptr itself has a size of two pointers, and the size of reference counter is, at least on my machine, the same as the size of two longs and two pointers.
And there’s the deleter thing. Yeah, no doubt it’s useful. Sometimes. But, again, you have to pay the price for having one even if you don’t really use it (wasn’t „don’t pay for what you don’t use” one of C++ design principles?). And this time, it’s impossible to turn it off.
One could claim that these all things certainly won’t be bottlenecks. I thought so, too. But I saw applications where shared_ptr logic took much time (up to 50% in one, somewhat extreme, case). So how can substituting intrusive_ptr for shared_ptr improve performance? intrusive_ptr can be thread-safe only where we want it to be (ranging from nowhere to whole project), because we’re obliged to provide addRef and release methods (actually their names are slightly different, and they’re free functions). Also, it doesn’t have to allocate anything on heap, because reference counter is a part of the object. And intrusive_ptr is as big as a raw pointer (because behind the scenes it is a raw pointer). Unfortunately, there’s no deleter, yet I bet it’s possible to implement it somehow if one really needs it.
Next issue with shared_ptr is that it’s amazingly easy to cause double deletion. Consider this:
Foo* rp = new Foo; shared_ptr<Foo> sp1(rp); shared_ptr<Foo> sp2(rp);
Both sp1 and sp2 will create a reference counter with count 1, and at the destruction both will try to delete the pointer. Oops, rp got deleted twice.
Surely, it is programmer’s fault. Too bad it’s easy to make the same (or similar) mistake in more complicated code. There is enable_shared_from_this, which allows to get a working shared_ptr from this. Alas, there’s no enable_shared_from_any or something. intrusive_ptr doesn’t suffer from this problem, because reference counter is embedded in the object itself. The object will know it’s referenced by two intrusive_ptrs so it’ll get deleted only once.
Yup, shared_ptr can point to any type, not necessarily a user-defined type, whereas intrusive_ptr can point only to types for which appropriate functions are defined. So shared_ptr can point to built-in types (I don’t think anyone uses heap-allocated built-ins unless it’s an array) and the types from third-party libraries. intrusive_ptr can point to the third-party types if they already have some way of reference counting, or if we wrap them. It’s not always feasible, of course. But it’s also possible to not use smart pointers for them at all – that can work pretty well.
Of course I know that sometimes shared_ptr is really the right way to go. But more often than not it’s not the best idea, because of the problems outlined above. If you’re considering reference counting, think about intrusive reference counting first, and only if it doesn’t suit your needs, go non-intrusively.
