2

Say I have

Animal animals[100];

And now, at run-time, I want to initialize one of these to be a new instance of Cat, a subclass of Animal. How do I do this? I thought maybe I could do

animals[0] = Cat("Fluffy")

but this doesn't seem to work. My understanding of how a constructor works in C++ is that space for the object is allocated, and then a pointer to that space is passed to the constructor function (as this). In particular, the constructor works even if that space contains any arbitrary garbage data. So it seems to me that even if animals[0] already contains data initialized by the constructor of Animal or whatever else was occupying that slot beforehand, it should be possible to just call the constructor of Cat on that space and have it work exactly as if it were a totally "fresh" object. How can I achieve this?

For example, the following code should print "Fluffy", but it prints "Anonymous".

#include <stdio.h>

class Animal
{
public:
    virtual char const *get_name() { return "Anonymous"; };
};

class Cat : public Animal
{
    char const *name;
public:
    Cat(char const *name) { this->name = name; }
    char const *get_name() { return name; }
};

Animal animals[100];

int main()
{
    animals[0] = Cat("Fluffy");
    printf("%s\n", animals[0].get_name());
    return 0;
}
Jack M
  • 3,286
  • 6
  • 29
  • 52
  • 3
    This is wrong because `animals[0] = Cat("Fluffy")` calls `Animal::operator=(const Animal&)`. https://stackoverflow.com/questions/274626/what-is-object-slicing Generally if you want a heterogeneous array you would need to store _pointers_ to the base class, or preferably a smart pointer, e.g. `std::unique_ptr animals[100];`, `animals[0] = std::make_unique("Fluffy")`. You could also use something like `std::variant`. – jtbandes Feb 09 '21 at 20:58
  • @jtbandes That seems out of sync with how simple what I'm trying to do seems to be, to me. Why do I need pointers? I *want* my data to be stored sequentially in memory, and for my use case I don't need or want to have to manage allocating and freeing. I don't see why the notion of subclassing should be coupled so tightly with whether you use pointers or direct references. – Jack M Feb 09 '21 at 21:01
  • 2
    Well, subclasses may have different sizes: your Cat object contains a `name` pointer, but Animal does not, so a Cat will not fit in space that was allocated only for an Animal. You will either need indirection provided by a pointer, or some kind of union (ensuring enough space is available; std::variant can do this, or std::aligned_union + placement new for a no-batteries-included approach). If you're storing multiple classes derived from the same base, a union will not allow you to use the common base interface, while a pointer will. – jtbandes Feb 09 '21 at 21:05
  • I'm not sure what you want to hear here, that's just the way it is. Imagine `Cat` had more data members than `Animal`. In that case, space for 100 `Animal`s was located but you couldn't fit 100 `Cat`s in there. – ProgrammingMachine5000 Feb 09 '21 at 21:06
  • You can check this with `static_assert(sizeof(Cat) > sizeof(Animal))`. – jtbandes Feb 09 '21 at 21:10
  • @jtbandes Oh, okay, I kind of assumed that C++ always treats superclasses as a union over all possible subclasses. – Jack M Feb 09 '21 at 21:12
  • Questions like this have also been asked several times, although I don't see a great canonical answer: https://stackoverflow.com/questions/20912106/c-array-of-base-class-which-has-instances-of-derived-classes-stored-in-the-ele, https://stackoverflow.com/questions/52301418/how-to-add-derived-class-objects-to-an-array-of-base-class-type, https://stackoverflow.com/questions/13048301/pointer-to-array-of-base-class-populate-with-derived-class – jtbandes Feb 09 '21 at 21:12
  • That's not possible! What if the base class is implemented in a system library and the derived class is in your program? For example, you might inherit `std::iterator` or `std:: enable_shared_from_this` and your derived class can have as many members as you like. – jtbandes Feb 09 '21 at 21:14
  • 1
    C++ brutally abuses folk who dare make assumptions about it. In this case, at the time `Animal` is compiled say only only `Cat` and `Dog` exist as subclasses. A while later, `Wombat` is added in a third-party library. There's no way for `Animal` to know of this new subclass. Because of this `Animal` only knows `Animal`. All derived classes are unknown. – user4581301 Feb 09 '21 at 21:17
  • 1
    Incidentally this is also why virtual destructors are important: https://stackoverflow.com/questions/461203/when-to-use-virtual-destructors – jtbandes Feb 09 '21 at 21:19

2 Answers2

2

You flat out can't do it the way you're trying to. When you create that array, you allocate exactly enough space to hold 100 Animals. Well, Cats might be a bigger object. There isn't space anymore.

Like some of the other comments, you would need to use a different data structure, probably one that is pointer-based.

Joseph Larson
  • 4,851
  • 1
  • 15
  • 25
2

This is wrong because animals[0] = Cat("Fluffy") calls Animal::operator=(const Animal&), which forgets about all the Cat-specific parts of the object. Poor Fluffy has been sliced

Generally if you want a heterogeneous array, you would need to store an array of pointers to the base class, or preferably a smart pointer, e.g.:

std::unique_ptr<Animal> animals[100];
animals[0] = std::make_unique<Cat>("Fluffy");

You could also use something like std::variant.

This is necessary because subclasses may have different sizes. For example, in your case, Cat contains a name pointer, but Animal does not, so a Cat will not fit in space that was allocated only for an Animal. To work around this, you will either need indirection provided by a pointer, or some kind of union to ensure enough space is available inline. (std::variant can do this, or std::aligned_union + placement new for a no-batteries-included approach).

If you're storing multiple classes derived from the same base, a union will not allow you to use the common base interface, while a pointer will, which is why I'd recommend a pointer here. Remember to give your base class a virtual destructor.

See also:

jtbandes
  • 104,858
  • 34
  • 217
  • 242