Tags

, ,

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.

B * b = create_b();

//with covariant returns
B * dup = b->clone();

//without covariant returns
B * dup = static_cast<B*>(b->clone());

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.

class A {
	virtual A * clone();
}
class B : public A {
	virtual B * clone();
}
class C : public B {
	virtual C * clone();
}

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.

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);
	}
};

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.

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();

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.

Advertisements