The undisputed magic of shared_ptr.
Lately there has been alot of talks about TR1 (Technical Report 1) and its adoption of Boosts shared_ptr. Now, I must admit that I like to control the lifetime of my objects myself thus I haven’t used it extensively, but I can see the benefits of writing sloppy code and being saved by the smart pointer. There is still one magic aspect of the shared_ptr that I don’t like, namely its ability to choose the correct destructor even when the base class’ destructor is non-virtual.
A small example of a typical bug:
class B{
public:
~B() { } // non-virtual destructor
};class D : public B
{
public:
~D() { };
};
// create instance of D (in a variable of type B*)
B* ptr = new D();
// delete the object
// This will call B::~B(), but not the derived D::~D()
// which usually results in some kind of erratic behaviour.
delete ptr;
Now, a workaround would be to cast the B* to a D* right before deletion, but who would think of that (wouldn’t it be easier to make the destructor virtual in the first place)?
// this will first call D::~D(), then B::~B() and everyone is happydelete (D*) ptr;
The shared_ptr solves this problem for you, it casts the pointer to the correct type before calling delete:
shared_ptr<B> ptr(new D());// Now, even if this is a shared_ptr<B>, upon deletion // it will be smart enough to delete it as a shared_ptr<D>
Pretty smart. Using some automagically template stuff the shared_ptr knows what type of object we’re dealing with, and even if the developer forget to declare the base class destructor as virtual the shared_ptr will clean up correctly.
What I don’t like? Well, it’s not the feature itself, nor is it the way it’s implemented (it’s actually intriguing to look at the way Boost work its way around this kind of sloppy code), it is the lack of information on how this works that worries me.
The ‘correct’ pointer type is determined upon initialization of the shared_ptr. It actually looks on the type of argument provided, then it carries this information with it, until is’t time to delete the object.
So, this is that critical line:
shared_ptr<B> ptr(new D());
Here you provide a D* as argument, and this will remembered for the lifetime of the shared_ptr ptr. An seemingly equal example is:
B* p = new D();shared_ptr<B> ptr(p);
But, wait! This snippet should behave the same ways as the previous snippet - but - the argument provided was in the form of a B*, thus D:~D() isn’t called upon deletion. For a C++ developer used to templates and template specialization this might not seem totally odd, but for a healthy person (like me) this modification, for no apparent reason, altered the behaviour of the program.
Another example:
B* p;shared_ptr<B> ptr(p = new D());
The result of the expression p = new D() might at first eyesight look like an D*, but infact it is a B*. Thus again D:~D() is skipped.
To be sure that you’re initializing the shared_ptr with the correct type you can ofcourse cast the argument to it’s actual type:
B* p = new D();shared_ptr<B> ptr(static_cast<D*>(p));
But again, this is not always possible. Lets say that you’re using some kind of frozen legacy code the implements the Factory Method Design Pattern. All you get from the factory it the type of the interface, not the implementation itself, and again the magic of shared_ptr will fail.
Now, my conclusion is that it’s time to emphasize on when you can count on the shared_ptr, and when you cannot. Or even better - when to use virtual destructors.












