Home > Mobile >  What is the correct way to make a polymorphic container using smart pointers?
What is the correct way to make a polymorphic container using smart pointers?

Time:08-30

I would like to make a C class that maintains a polymorphic container. For example, a vet can hold a list of pets currently receiving treatment. If we use raw pointers, we could define a vet as follows.

class Vet{
    std::vector<Pet*> pets;
public:
    void addPet(Pet* pet);  
};

We could add pets as follows.

Vet vet;
vet.addPet(new Dog{});
vet.addPet(new Cat{});

In this case, the destructor of Vet class should be made responsible for deleting the dynamically allocated pets maintained in pets. To avoid this, I would like to use smart pointers. However, there are some issues which need clarification before I can correctly and cleanly implement such code. Need help on following issues. Thanks in advance.

  1. Should I use std::unique_ptr or std::shared_ptr? or in which circumstances should I use std::unique_ptr or std::shared_ptr? or should I go back to using raw pointers?
  2. If I choose to use std::unique_ptr what would be the method signature of addPet method and its implementation?
  • choice 1-1)
void addPet(std::unique_ptr<Pet> pet){
    pets.push_back(std::move(pet));
}
  • choice 1-2)
void addPet(std::unique_ptr<Pet>& pet){
    pets.push_back(std::move(pet));
}

This choice only works if I construct a pet as follows.

std::unique_ptr<Pet> dog = std::make_unique<Dog>();
vet.addPet(dog);
  • choice 1-3)
void addPet(PetType pet){
    if(pet==PetType::Dog) pets.push_back(std::make_unique<Dog>());
    //
}
  1. If I choose to use std::shared_ptr what would be the method signature of ```addPet''' method and its implementation?
  • choice 2-1)
void addPet(std::shared_ptr<Pet> pet){
    pets.push_back(std::move(pet));
}
  • choice 2-2)
void addPet(const std::shared_ptr<Pet>& pet){
    pets.push_back(pet);
}

CodePudding user response:

Assuming you really want to create the pets on the outside, the best way forward would be unique_ptr and passing it by value, like below:

void addPet(std::unique_ptr<Pet> pet);

unique_ptr because you're telling readers that there is only a single owner.

By value, because that would require you to either use a temporary or explicitly move the unique_ptr when calling addPet. Making it very explicit on the outside that you're moving/transferring ownership.

E.g. you could either do

auto pet = std::make_unique<Dog>();
vet.addPet(std::move(pet)); // move is required, making it explicit that ownership is transferred

or

vet.addPet(std::make_unique<Dog>());

If you would accept a reference to a unique_ptr in addPet the caller would not know whether the parameter that is provided is still valid after the call to addPet or not.

Here's a great post about the subject.

CodePudding user response:

Use unique_ptr. shared_ptr is for sharing ownership, but you want the vet to own the pets it contains, ie no ownership is shared.

The unique_ptr needs not be present at the interface. The public interface to add cats and dogs can be this:

class vet {
    public:
       void add_dog(const std::string& name);
       void add_cat(const std::string& name);
};

Use unique_ptr as function argument when you want to transfer ownership. There is no need to first let the caller own the pets and then let them transfer ownership to the vet....unless thats what you want, then do use std::unique_ptr<Pet> as argument (no reference, but by value).

CodePudding user response:

If not necessary don't implement resource control that is already available. You don't have to have the user construct the object, necessarily, which adds the benefit that you don't expose to the user what data structures you use internally.

class Vet {
  private:
    // Make sure Pet::~Pet() is virtual!
    std::vector<std::unique_ptr<Pet>> pets;
  public:
    template <typename Species, typename... Args>
    void addPet(Args...&& args) {
      pets.emplace_back(std::make_unique<Species>(std::forward<Args>(args)...));
    }
};

// ...

vet.addPet<Cat>(3, "black"); // assuming Cat::Cat(unsigned age, std::string color)

Note however, that semantically, a Vet does not own a pet, they take care of it for a while and release the pet afterwards. A vet that destroys a pet after they're done treating it would be a bad vet.

Anyhow, if you want access to a pet, you can easily add a similar member function:

Pet& getPet(std::size_t const i) {
  return *pets[i];
}
template <typename Species>
Species& get(std::size_t const i) {
  return static_cast<Species&>(getPet(i));
  // maybe consider dynamic_cast, here
}
  • Related