1

I want to have a unique_ptr as a class variable to support polymorphism. I have the class built but I cannot use the std::vector constructor because the std::unique_ptr copy constructor is explicitely deleted. Here's an abstracted example:

#include <iostream>
#include <vector>
#include <memory>

using namespace std;

class Animal {

protected:
    std::string noise = "None";
public:
    Animal() = default;

    virtual std::string getNoise() {
        return noise;
    }

};

class Duck : public Animal {
public:
    Duck() {
        noise = "Quack!";
    }
};

class Dog : public Animal {
public:
    Dog() {
        noise = "Woof!";
    }
};

typedef std::unique_ptr<Animal> AnimalPtr;


class Zoo {
public:
    AnimalPtr animalPtr;

    explicit Zoo(AnimalPtr animalPtr) : animalPtr(std::move(animalPtr)){};

    explicit Zoo(const Animal& animal) : animalPtr(std::make_unique<Animal>(animal)){};

    const AnimalPtr &getAnimalPtr() const {
        return animalPtr;
    }

};

int main() {

    Zoo zoo1((Dog()));
    Zoo zoo2((Duck()));

    std::vector<Zoo> zoos = {zoo1, zoo2}; // error, Call to implicitly-deleted copy constructor of 'const Zoo'

    return 0;
};

I could solve this problem by using a std::shared_ptr instead, but something tells me this isn't the correct reason for allowing shared ownership. So my question is what is the correct way to solve this problem? (i.e. to allow me to construct a std::vector of animals.

CiaranWelsh
  • 7,014
  • 10
  • 53
  • 106
  • 1
    Does this answer your question? [Can I list-initialize a vector of move-only type?](https://stackoverflow.com/questions/8468774/can-i-list-initialize-a-vector-of-move-only-type) – Botje Apr 16 '20 at 09:43

4 Answers4

3

Since C++11, a std::vector can perfectly accommodate an object that can't be copied. However not all methods of the vector can be used. In particular the initializer_list constructor that you are invoking to initialize your vector, do not allow them which is conter-intuitive, I admit. In current standard intializer_list always work by copy and never by move, this may change later I suppose. Here is the constructor signature:

vector(std::initializer_list<T> init, const Allocator& alloc = Allocator());

In anycase, by not wrapping zoo1 and zoo2 by std::move you asked it to do a copy of zoo1 and zoo2 anyway to build the std::initializer_object

However you can still use the default constructor that do not require the value to be copyable, and then do some push_backs, like

std::vector<Zoo> zoos;
zoos.push_back(std::move(zoo1));
zoos.push_back(std::move(zoo2));

As mentionned in rustyx answer on can also use emplace_back to directly build your Zoo object from any Animal in place inside the vector instead of moving an already built Zoo inside

  • it's worth to add that std::move is allowing the efficient transfer of resources from object to another object and after moving the "source object - moved" will be empty, although the resource of object will be available inside vector. – Ziumper Apr 19 '23 at 07:49
1
class Zoo {
public:
AnimalPtr animalPtr;

explicit Zoo(AnimalPtr animalPtr) : animalPtr(std::move(animalPtr)) {};

explicit Zoo(const Animal& animal) : animalPtr(std::make_unique<Animal>(animal)) {};

Zoo(const Zoo& obj) : animalPtr(make_unique<Animal>(*obj.animalPtr)) {}

const AnimalPtr &getAnimalPtr() const {
    return animalPtr;
}

};

If you declare the copy constructor like mentioned above then the file would compile successfully keeping the bellow statement intact.

std::vector<Zoo> zoos = { zoo1, zoo2 };

  • I think this is the answer I was looking for. But would it be considered a "hack"? – CiaranWelsh Apr 16 '20 at 10:51
  • 1
    I don't think so. So just test if this safe or not, I created another container for vector and added the same zoo1 and zoo2 object using emplace_back. For both the classes the ctor got called once. If the ctor would got called twice this would have been a problem/hack. It is more or less the same like your ctor for explicit Zoo(const Animal& animal). – Debojyoti Majumder Apr 16 '20 at 11:03
0

The problem is that initializer-list in vector construction makes copies because initializer-list passes elements by a const-reference.

You can bypass it by adding elements manually:

std::vector<Zoo> zoos;
zoos.emplace_back(Dog());
zoos.emplace_back(Duck());
rustyx
  • 80,671
  • 25
  • 200
  • 267
  • I appreciate that this is a way around (though I'm not sure why you have used `emplace_back` rather than `push_back`, I'll look this up). However, instead of blindly using a shared_ptr, or forcing my users to construct a `std::vector` in a specific way, I wanted to know if there was a "right" way to solve this problem. If the answer is no, I'm happy to use this solution. Thanks for the answer. – CiaranWelsh Apr 16 '20 at 09:52
  • 1
    I don't think there is a right way. The braced-init-list syntax looks nice and concise but won't work as I explained, because it wants to make copies and `Zoo` is not copyable. Switching to `shared_ptr` will make the braced-init-list version appear to work, but it will work by performing unnecessary copying behind the scenes (i.e. inefficient). The choice is yours - efficiency (with unique_ptr) or convenience (with shared_ptr). – rustyx Apr 16 '20 at 10:04
  • I appreciate the advice. – CiaranWelsh Apr 16 '20 at 10:48
0

Since ownership of animal pointers is Zoo class (one unique owner). It does not really make sense to "copy construct" a vector of Zoos (You would have to move the Zoo instances into the vector). Why not simply declaring:

std::vector<Zoo*> zoos = {&zoo1, &zoo2};
Jean-Marc Volle
  • 3,113
  • 1
  • 16
  • 20