0

My understanding of C++ arrays is that you can't allocate an array of abstract class objects since C++ doesn't know how to allocate memory for a yet-to-be-decided class type.

I put together a little example that confuses me a bit, so wanted to ask a bit more

#include <iostream>

class Animal {
public:
  virtual void hello() {}
};

class Dog : public Animal {
public:
  void hello() { std::cout << "woof!" << std::endl; }
};

class Cat : public Animal {
public:
  void hello() { std::cout << "meow" << std::endl; }
};

int main() {
  Dog d;
  d.hello(); // prints "woof!"

  Cat c;
  c.hello(); // prints "meow"

  // how are we allowed to create an array of abstract class?
  // doesn't c++ need to know how to allocate memory for any abstract
  // class in order to do this?
  Animal creatures[5];
  creatures[0] = d;
  creatures[1] = c;
  creatures[4] = d;

  // prints "6Animal"
  std::cout << typeid(creatures[0]).name() << std::endl;

  // this appears to call the Animal hello(), which does nothing
  creatures[1].hello();
}

Questions

  1. How is C++ able to allocate memory for this array? Why doesn't it complain?
  2. It appears something about this not failing is due to treating all the objects as Animals, ie: not properly doing polymorphism. What exactly is going on, and why? Do I just have to allocate for a list of pointers to do this properly instead?

Thanks!

lollercoaster
  • 15,969
  • 35
  • 115
  • 173

1 Answers1

4

Animal is not abstract. It contains no pure virtual member functions. When you assign c and d to the elements of creatures you are slicing them.

If instead, Animal::hello had been declared pure-virtual, i.e.

class Animal {
public:
  virtual void hello() = 0;
};

Animal creatures[5] would fail to compile since Animal is now abstract.


As per your second question, runtime polymorphism in C++ only works with references and pointers. If you're familiar with languages like Java or Python this can seem a bit odd at first, but remember that in those languages all variables of class types are pointers (or pointer-like things, anyway).

In C++, Animal creatures[5] will be laid out in memory something like this:

creatures
+--------+--------+--------+--------+--------+
| Animal | Animal | Animal | Animal | Animal |
+--------+--------+--------+--------+--------+

In Java, Animal[] creatures = new Animal[5]; will be laid out in memory like this:

+-----------+   +---+---+---+---+---+
| creatures +-->+ 0 | 1 | 2 | 3 | 4 |
+-----------+   +-+-+-+-+-+-+-+-+-+-+
                  |   |   |   |   |
       +--------+ |   |   |   |   | +--------+
       | Object +<+   |   |   |   +>+ Object |
       +--------+     |   |   |     +--------+
                      v   |   v
               +------+-+ |  ++-------+
               | Object | |  | Object |
               +--------+ |  +--------+
                          v
                     +----+---+
                     | Object |
                     +--------+

There is no direct analogue for C++ arrays in languages like Java or Python

That means that all objects in a C++ array must be the exact same type. If you want to build something like the Java array, you must use pointers. You should use the standard smart-pointer classes std::unique_ptr and std::shared_ptr so that the allocated memory gets automatically cleaned up, since C++ does not have a garbage collector. i.e.

std::shared_ptr<Animal> creatures[5];
creatures[0] = std::make_shared<Dog>();
creatures[1] = std::make_shared<Cat>();

creatrues[0]->hello(); // prints "woof!"
creatures[1]->hello(); // prints "meow"

Live Demo

Miles Budnek
  • 28,216
  • 2
  • 35
  • 52
  • Ah, I see. So how do we best achieve polymorphism in this case? do we have to resort to pointers? – lollercoaster Mar 19 '20 at 21:11
  • Yes, runtime polymorphism in C++ only works with pointers and references. See my edit. – Miles Budnek Mar 19 '20 at 21:14
  • 1
    I would recommend changing the `std::shared_ptr` to a `std::unique_ptr`. The default should always be `std::unique_ptr`; you don't want a shared pointer unless you know for a fact that there will be a multiple ownership scenario. Even then, you might not need it. – sweenish Mar 19 '20 at 21:25
  • @sweenish I agree `std::unique_ptr` should be the default, but I used `std::shared_ptr` to be as similar to the Java example as possible (Java references work via entirely different mechanisms, but they also allow multiple-ownership). – Miles Budnek Mar 19 '20 at 21:28