Inheritance Lab
Goals
To be able to use inheritance in C++ effectively.
Setup
-
Open the Inheritance Lab assignment on Moodle to get the Git repository template.
- Open Terminal and clone your repository:
git clone <your repository link>
-
Open VSCode and then open your repository folder.
-
Open a Terminal in VSCode and pull down the submodules for the starter code:
git submodule update --init --recursive
(We won’t be using Emscripten in this lab so you don’t need to set it up.)
Exercise 1
For this lab, you’ll make an Animal
superclass and a couple of subclasses of it. Let’s start with Animal
.
-
Make a new file
Animal.h
and define anAnimal
class with a private instance variableage
:class Animal { int age; };
-
Create a constructor for your class and set the age from a parameter. Remember that you can do this in the fancy C++ way like so:
public: Animal(int _age) : age(_age) { //doesn't do anything else }
-
We want to be able to easily tell which class is being accessed, so also make a method
GetType()
that returns the string “Animal”:std::string GetType() {return "Animal";}
Exercise 2
Now we need to make a subclass to experiment with.
-
Create a new file for a
Dog
subclass. In that subclass, define aDog
class that inherits fromAnimal
(you’ll need to#include Animal.h
as well):class Dog : public Animal { };
-
Let’s have our dog have an additional instance variable
num_spots
. To allow our dog’s constructor to set both num_spots and age, we need to take them both as parameters and then set them in two slightly different ways:int num_spots; public: Dog(int _age, int _num_spots) : Animal(_age), //Calls Animal constructor and gives it age num_spots(_num_spots) { //Sets num_spots //Doesn't do anything else }
This code is calling the
Animal
constructor and passing the_age
variable to it sinceAnimal
is in charge of that variable. Then it sets the dog specific variable. -
Also create a
GetType()
method that returns “Dog” so that we can tell the difference easily.
Exercise 3
Okay, let’s see what we get with this setup.
-
In
main.cpp
, create an Empirical vector of Empirical pointers toAnimal
calledpopulation
:emp::vector<emp::Ptr<Animal>> population;
-
Create a new
Dog
instance and add it to the vector. Note that because the vector holds pointers to the superclass ofDog
, it’s fine to putDog
objects in it:population.push_back(new Dog(5, 2));
-
While you could get away with just including
Dog.h
, this is a good time to observe and fix one problem you’ll eventually run into. At the top of your file, make sure you have included bothAnimal.h
andDog.h
:#include "Animal.h" #include "Dog.h"
-
Then run the
compile-run.sh
script. You should get an errorredefintion of Animal
. This is becauseAnimal
is included inDog
, and so C++ thinks you are trying to define theAnimal
class twice. The slick way of getting around this is with the use of C++ macros, which are instructions run by the preprocessor. At the top ofAnimal.h
, add the following:#ifndef ANIMAL #define ANIMAL
This macro is saying “check if the ANIMAL name is already defined, if it isn’t, define it and run all the following code”, which means that if it’s already been defined, the code won’t run!
-
You also need to close your if at the very end of your
Animal.h
file:#endif
-
You should now be able to compile and run without a problem.
Exercise 4
You need to now check which class’ methods are getting accessed (and make some fixes).
-
In
main.cpp
, call theGetType()
method of the only item in your population and print it out:std::cout << population[0]->GetType() << std::endl;
-
It should be printing “Animal” meaning it’s calling the
Animal
method instead of theDog
method! Because C++ “bundles” both types together when it makes a subclass object, it is defaulting to the superclass method since that is what the vector knows that it is holding. You can tell C++ that you want it to actually default to the subclass method with thevirtual
keyword. InAnimal.h
add the keywordvirtual
before the return type of theGetType()
method:virtual std::string GetType() {return "Animal";}
-
Now when you compile and run, you should get “Dog” printing out. This means that anytime you have a superclass method that you want the base class method to be called instead, you should put
virtual
in front of it. -
This process only works because these are pointers! If you ever dereference the pointer and save it to an
Animal
variable, C++ cuts off theDog
portion. Test this out inmain.cpp
:Animal test = *population[0]; std::cout << test.GetType() << std::endl;
Exercise 5
What about those instance variables that you made? How do the two classes deal with those?
-
If we want to be able to get access to the
age
variable, we’ll need an accessor method, since it’s private (and should stay that way). Make aGetAge()
method inAnimal
:int GetAge() {return age;}
-
What if you want to print out the dog’s age? You didn’t define a
GetAge()
function for theDog
class, but you didn’t have to, because the super class is in charge of that! Inmain.cpp
call and print out the age of your dog usingGetAge()
:std::cout << population[0]->GetAge() << std::endl;
This is the great thing about inheritance: you can have functionality in
Animal
that is shared across all the subclasses (once you have more of them) and only need to implement it once (and you can keep them all in the same vector). -
What if you wanted to make a custom
GetAge
method for dog, for example to express the dog’s age in dog years (multiplied by 7) instead? You can’t access theage
variable directly since it is private to theAnimal
class. Instead, you need to use the same accessor method, but specify that you want to call the super class version inDog
’sGetAge()
method:int GetAge() { return Animal::GetAge() * 7; }
-
Try compiling and running your code again. Are you getting the correct age calculation? Probably not if you forgot to go back to
Animal
and put in thevirtual
keyword. Go do that and make sure that you are getting the correct age now.
Exercise 6
What about when you want your animals to make new animals? Time to make a reproduction method and think more about how subclasses interact with superclasses!
-
Make a virtual
Reproduce
method in theAnimal
class that return a newAnimal
(you could argue that this shouldn’t do anything, or throw an exception, but we’re assuming that maybe you do want to make just animals sometimes):virtual emp::Ptr<Animal> Reproduce() { return new Animal(0); }
-
Go into your
Dog
class and make aReproduce
method. It needs to return the same type asAnimal
, but it should make a dog, because we can again make an actual instance of typeDog
, but pass it around as anAnimal
without a problem. Note also that you can use theDog
specific methodGetSpots()
without a problem because it is aDog
instance and C++ knows that at this point:emp::Ptr<Animal> Reproduce() { return new Dog(0, GetSpots()); //assuming it should have the same number of spots as parent }
-
In
main.cpp
, call theReproduce
method of your existing dog and add the offspring to the population, then make sure that it is the correct type:population.push_back(population[0]->Reproduce()); std::cout << population[1]->GetType() << std::endl;
Exercise 7
What if you want to access dog specific methods? While it’s generally best to avoid needing to access methods special to the subclasses in this kind of setup, sometimes you need to. Fortunately, Empirical’s pointer makes that fairly easy to do.
-
First, let’s see the problem. In
main.cpp
try to print the number of spots of your dog:std::cout << population[0]->GetSpots() << std::endl;
-
You should be seeing an error that
Animal
has no memberGetSpots()
. This is because C++ only knows that this object is of typeAnimal
, it doesn’t know that it’s actually aDog
. Because you know for sure that the object is a dog at this point in the code, you can cast it to typeDog
and then call theDog
specific method (you should only do this when you know for sure!):std::cout << population[0].DynamicCast<Dog>()->GetSpots() << std::endl;
-
You should avoid dynamic casting whenever you can, and instead override methods of the superclass for subclass specific behavior. So for example, we could have a
GetAttributes
method that allAnimals
have and gets whatever the subclass specific attributes are.
Exercise 8
The big reason that you’re going to be wanting to use inheritance is so that you can have objects of multiple different types all in the same vector (the pop
vector of your world), but you aren’t actually doing that right now. Create another subclass of Animal
and play around with having both it and Dog
s in the same vector and accessing their methods correctly.