Converting unique_ptr to shared_ptr: Factory function example

Question | Nov 25, 2019 | nextptr 

Background

The std::unique_ptr and std::shared_ptr are smart-pointers. An std::unique_ptr owns an object exclusively, whereas the ownership of an object can be shared via std::shared_ptr instances.

One of the helpful features of unique_ptr is that it can be seamlessly converted to a compatible shared_ptr. The conversion is possible through an std::shared_ptr<T>'s constructor that takes an rvalue reference of std::unique_ptr<Y, Deleter> type and moves it:

//shared_ptr constructor that takes a unique_ptr&&
template<typename Y, typename Deleter>
shared_ptr(std::unique_ptr<Y,Deleter>&& u);

The Y* must be compatible with T* for the conversion to be possible. The unique_ptr (u) is moved and does not own the object after the conversion.

Below are some trivial examples of converting a unique_ptr to a shared_ptr. Note that a shared_ptr is constructed below through a temporary rvalue or by explicitly moving an lvalue:

//'foo()' returns a unique_ptr
std::unique_ptr<std::string> foo();

//from a function result. Temporary is moved.
std::shared_ptr<std::string> sp1 = foo(); // OK

//A unique_ptr<std::string> initialized
auto up = std::make_unique<std::string>("Hello World");

// Move an lvalue unique_ptr to shared_ptr
std::shared_ptr<std::string> sp2 = std::move(up); // OK

One practical use of this feature is in the factory function pattern, which often produces an instance and returns a pointer to it. A factory method implementation cannot assume whether the ownership of the returned object would be shared, or it would be owned exclusively. One way is to return a raw pointer and let the callers of the factory function wrap the raw pointer in a shared_ptr or a unique_ptr. But the drawback of that approach is that we have to renounce the std::make_unique (or std::make_shared). Moreover, the use of a raw pointer inside a factory method is unsafe.

A more flexible design is to return a unique_ptr from a factory method, which can be used as-is or can be converted to a compatible shared_ptr by the callers.

An Example and The Question

Consider a polymorphic class hierarchy, as shown below:

struct Base { 
 // ...
 virtual ~Base() { }
};
struct Derived1 : public Base {
 // ...
 ~Derived1() { }
};
struct Derived2 : public Base {
 // ...
 ~Derived2() { }
};

A factory function creates an object of type Derived1 or Derived2 depending on the parameter t and returns an std::unique_ptr<Base>:

std::unique_ptr<Base> factory(int t) {
 switch(t) {
   case 1:
     return std::make_unique<Derived1>();
   case 2:
     return std::make_unique<Derived2>();
   default:
     return nullptr;
 }
}

Below are a few cases of converting the result of the factory function to an std::shared_ptr:

A

std::shared_ptr<Base> ptrBase = factory(1);

B

std::shared_ptr<Derived2> ptrDerived2 = factory(2);

C

std::shared_ptr<void> ptrVoid = factory(1);

One or more of the above cases are invalid (would not compile) because of the use of incompatible std::shared_ptr.

Select below all the valid cases from above (check Explanations for details):


Afterword

The flawless conversion of an std::unique_ptr to a compatible std::shared_ptr makes it possible to write efficient and safe factory functions. However, note that an std::shared_ptr cannot be converted to an std::unique_ptr.

References

Constructors of std::shared_ptr - cppreference

The std::shared_ptr<void> as arbitrary user-data pointer - nextptr

shared_ptr - basics and internals with examples - nextptr