c++polymorphismfactorysmart-pointers

When is it appropriate to assign a unique_ptr to a shared_ptr?


This is a follow up to the question that was asked here: Does C++11 unique_ptr and shared_ptr able to convert to each other's type? and more specifically the top voted answer.

The snippet that caught my attention was this:

This is a key part of why std::unique_ptr is so well suited as a factory function return type. Factory functions can’t know whether callers will want to use exclusive ownership semantics for the object they return or whether shared ownership (i.e., std::shared_ptr) would be more appropriate. By returning a std::unique_ptr, factories provide callers with the most efficient smart pointer, but they don’t hinder callers from replacing it with its more flexible sibling.

And then I saw a duplicate question is assigning unique_ptr to shared_ptr allowed? that had a comment:

Why shouldn't be? It's even safer than assigning regular pointer...[more text here].

However the user did not elaborate why it was safer than assigning a regular pointer. I am wondering in what cases would it be best to assign a unique_ptr to a shared_ptr? If I understood correctly, the first quoted section, cases like this are the best: (Let's assume that we have a working base class Car and all of the CarTypes inherit from Car.)

enum class CarType
{
    Sedan,
    SUV,
    Crossover,
    Truck,
    Bus
};

class MyRental
{
  public:
    MyRental();

    void assignRentalCar(CarType type);
    void doDriveAction();
    void doTurnAction();
    void doParkAction();

  private:
    std::shared_ptr<Car> currentCar;
    std::shared_ptr<Bus> bus; // a Oversized Vehicle Bus : (public OversizedCar : public Car)

};

From above in our private members lets say we have a special case where the member variable, bus, inherits from an OversizeCar class which inherits from a Car class.

MyRental::MyRental() : currentCar(std::make_unique<Sedan>()),
                       bus(std::make_unique<Bus>())
{}
                         

void MyRental::assignRentalCar(CarType type)
{
  switch(type)
  {
    case CarType::SUV: currentCar = std::make_unique<SUV>(); break;
    case CarType::Crossover: currentCar = std::make_unique<Crossover>(); break;
    case CarType::Truck: currentCar = std::make_unique<Truck>(); break;
    case CarType::Bus: currentCar = bus; break; // allowed because currentCar is a shared_ptr
    case CarType::Sedan:
    case default: currentCar = std::make_unique<Sedan>(); break;
  }
}

void MyRental::doDriveAction()
{
  currentCar->handleDrive();
}

void MyRental::doTurnAction()
{
  currentCar->handleTurn();
}

void MyRental::doParkAction()
{
  currentCar->handlePark();
}

Is this an appropriate case to assign a unique_ptr to a shared_ptr? And if so why is it appropriate? If it is not appropriate and better to assign shared_ptrs instead, when is it best to assign a unique_ptr to a shared_ptr?


Solution

  • I am wondering in what cases would it be best to assign a unique_ptr to a shared_ptr?

    You're overthinking this, but I suppose it is a valid question. The scenarios where an assignment is reasonable are when the following hold.

    1. You have a value, u, of type U to assign from.
    2. You have an object, s, of type S to assign to.
    3. The assignment s = u is syntactically valid and useful for your code.
    4. The construction S s(u) is not a reasonable option. If you can reasonably use construction instead of assignment, do that.

    These criteria are good for many types, including when U is std::unique_ptr&& and S is std::shared_ptr. One caveat for the smart pointer case is that the assignment must be from an rvalue. That is, if the unique_ptr is not being returned by a function, you probably have to move it, as in s = std::move(u). Note that this move assignment will result in u being null; you will not have s and u point to the same object at the same time.

    Basically, use assignment when it is natural, and avoid it when it is not.


    You did ask about "best" rather than "good", though. That brings in another consideration, also not particular to smart pointers: avoid unnecessary conversions. Assigning a unique_ptr<Truck> to a shared_ptr<Car> involves a more complex conversion than converting a shared_ptr<Truck> to a shared_ptr<Car>. In your example, you could readily simplify by replacing std::make_unique with std::make_shared. Keep it simple, and don't push others to ask "does a unique_ptr convert to a shared_ptr?"

    Basically, stick to shared_ptr when that is an option.