Smart pointers in C++ are nice, yet fraught with irregularities. One is the inability to create a proper clone function. This requires a feature called covariant return types, which C++ supports, but only for pointers. That sounds okay, but despite their name, smart pointers are not actually pointers.
Covariant return
A prototypical example of covariant return types is a clone method. Reasonably, we expect that a ‘clone’ method returns the type on which it was called. It wouldn’t make sense to call it ‘clone’ if it could return an incompatible object type.
[sourcecode language=”cpp”]
B * b = create_b();
//with covariant returns
B * dup = b->clone();
//without covariant returns
B * dup = static_cast<B*>(b->clone());
[/sourcecode]
The above covariant call can be implemented as below. Here we see a derived class overriding the virtual function with a different return type. This is well defined so long as the return is a derived type.
[sourcecode language=”cpp”]
class A {
virtual A * clone();
}
class B : public A {
virtual B * clone();
}
class C : public B {
virtual C * clone();
}
[/sourcecode]
The caller of clone is returned the same type without having to cast it. The virtual function still works as expected: if you have a ‘B*’ to an object of type ‘C’, the ‘C::clone’ method will be called and its return cast to a ‘B*’. Beyond just a convenience, it is safer to have the compiler do this than rely on a ‘static_cast’. As code changes over time, one of those ‘static_cast’ statements will inevitably perform an invalid cast.
Broken with shared_ptr
I consider ‘shared_ptr’ to be a good option for general purpose object management. Unfortunately it isn’t a real pointer. Only via a series of implicit conversion functions does it behave somewhat like a pointer. Weaknesses show up in many situations: covariant returns are one of them. It is not possible to code a ‘clone’ function as expected.
[sourcecode language=”cpp”]
struct A {
virtual shared_ptr<A> clone() {
return make_shared<A>(*this);
}
};
struct B : public A {
shared_ptr<B> clone() {
return make_shared<B>(*this);
}
};
[/sourcecode]
Covariant returns are restricted to true pointer types. The above code results in a compiler error. Using a ‘smart_ptr’ requires returning the same type in all overrides of the virtual function. Using these non-covariant clone methods requires an unsafe cast.
[sourcecode language=”cpp”]
shared_ptr<B> b = create_b();
//requires this unsafe syntax
shared_ptr<B> ugh = static_pointer_cast<B>( b->clone() );
//the preferred syntax
shared_ptr<B> good = b->clone();
[/sourcecode]
Using macros and/or other template techniques the overhead can be reduced somewhat. But it’s code I shouldn’t have to write. Indeed, a minor error in one of my versions resulted in a hard-to-find defect. It’s a defect that happened because a new C++ feature, smart pointers, are effectively incompatible with a previous feature, covariant returns. I’m in favour of new language features, but they should expand on previous features, not deprecate them. As it stands, a proper clone function using smart pointers cannot be written.