Sunday, 5 June 2016

Converting shared_ptr of Derived to shared_ptr of Base

Years and years ago, as C++ templates were gaining prominence, the common problem users had with them was to understand how the types of particular template instantiations were related. E.g. given the following classes:
  class Base {};
  class Derived : public Base {};

  SomeTempl<Base> tb;
  SomeTempl<Derived> td;
it was somehow non-intuitive that the class of the td object was not a subcalsss of tb. But well, that wasn't that different form the basic language rules for array types:
  Base[100] ab;
  Derived[100] ad;
Here ab isn't in any way related to ad. So some user wished that templates may be somehow related on their parameters but it was an easy task to persuade them, that they were plain wrong. And taking into account that in those distant times templates were used as implementation helper to define containers, this wasn't even that wrong.

Well, until...

You guessed it, until C++11 arrived, and the "C++ renaissance" and "modern C++" was announced. As you might know, in "modern" C++ you shouldn't use naked pointers, instead, you use the appropriate smart pointer class unique_ptr<>, shared_ptr<> or weak_ptr<>.

Ooops, now we don't have a container on our hands! It's rather a kind of decorator, taking a type, and creating a new, but semantically not really different one. What I mean here is, that in this case, we have a warranted need for template types to be related based on their arguments! I.e.:
  unique_ptr<Base> pb;
  unique_ptr<Derived> pd;
pd should behave as subclass of pb, because the unique_ptr<> "decorator" template's intention is to preserve the argument type's pointer semantics! So what now? Language rules for templates cannot be changed, the types cannot be related, so if you want to write covariant member function using pointer types (like here:)
  class X
  public: Base* calculemus();

  class Y : public X
  public: Devived* calculemus();
you cannot use smart pointers instead. That brought us a slew of Stack Overflow questions of general type "How to convert shared_ptr<> to...".

So indeed, what can be done? How to convert one to another? As I said before, template rules cannot be changed at that point. So if not in language definition, so maybe we can solve that in library?

Well, let's try. If we take, for example, a close look at the definition of unique_ptr<T> in the C++11 standard (through this time) we can find the following converting constructor:
template< class U, class E >
unique_ptr( unique_ptr<U, E>&& u );    (6)
6) Constructs a unique_ptr by transferring ownership from u to *this
This constructor only participates in overload resolution if all of the following is true:
  a) unique_ptr<U, E>::pointer is implicitly convertible to pointer
  b) U is not an array type
  c) Either Deleter is a reference type and E is the same type as D, or Deleter is not a reference type and E is implicitly convertible to D
We see, the converting constructor has to be enabled by SFINAE when the the pointer conversion is sound (probably by std::is_derived or something like that). Nice!

So the right way of converting a unique_ptr<Derived> to unique_ptr<Base> is to assign the later to the former! I knew, the C++ committee wouldn't let us in the lurch!

PS: And if you want to know how to use that possibility to somehow emulate covariant return types through smart pointers, have a look at Arne Mertz's blogpost:

No comments: