2

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);
}
Sangjin Kim
  • 991
  • 1
  • 6
  • 8
  • 2
    I only use shared_ptr for things with hard to control life times at runtime (e.g. when sharing data between threads), but I prefer to know my lifecycles at design time. Then the other question you have to answer is will your vector OWN the polymorphic objects or just refer to them? If it is a non-owning relation you can use pointers to the baseclass, if it is owning use std::unique_ptr – Pepijn Kramer Aug 29 '22 at 14:03
  • 1
    There are a few different questions here with interdependencies on the answer. My suggestion is to read up more on the differences between `unique_ptr` and `shared_ptr`. There's lots out there already. Here's a good starting point: https://stackoverflow.com/questions/6876751/differences-between-unique-ptr-and-shared-ptr – AndyG Aug 29 '22 at 14:10
  • Choice 2-2 is the more flexible you can adopt... you don't need 2-1 with shared pointer and you can call it as `vet.addPet(std::make_shared());` – Marco Beninca Aug 29 '22 at 14:15

3 Answers3

2

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.

Tohnmeister
  • 468
  • 1
  • 5
  • 14
1

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
}

Also, keep in mind that polymorphy is not the only choice here:

template <typename... Pets>
class Vet {
  using value_type = std::variant<Pets...>;
  std::vector<value_type> pets;
  
  public:
    template <typename Species, typename... Args>
    void addPet(Args&&... args) {
      pets.emplace_back(Species{std::forward<Args>(args)...});
    }
};

A Vet<Dog, Cat> can treat dogs and cats but a Vet<Snake, Pigeon> can treat snakes and pigeons. They don't even need to have a common base type.

bitmask
  • 32,434
  • 14
  • 99
  • 159
  • ```void addPet(Args...&& args)``` should be ```void addPet(Args&&... args)```. Using a vet as an example was a bad choice. – Sangjin Kim Aug 30 '22 at 00:44
  • @SangjinKim Yes, what a silly mistake. Fixed. – bitmask Aug 30 '22 at 01:09
  • Why do you use ```emplace_back``` instead of ```push_back``` in the polymorphic example? In typical ```emplace_back```, we normally provide constructor arguments, but here ```std::unique_ptr``` is constructed as the argument. – Sangjin Kim Aug 30 '22 at 04:56
  • 1
    @SangjinKim push_back would also work. There is no big difference in this case. – bitmask Aug 30 '22 at 08:53
0

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).

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185