Notes on Inheritance and Polymorphism

Introduction

many object-oriented languages have a feature called inheritance

inheritance lets a class inherit the variables and methods from another class

among other things, this lets you create new classes by adding functionality to existing ones

inheritance also lets you specify methods that a class must have without saying how those methods must be implemented

this last point, in particular, turns out to be extremely useful in practice

Using Multiple Classes

in large programs, you often uses many different classes

for instance, imagine a program with these three classes: Point, Person, and Reading

#include "cmpt_error.h"
#include <iostream>

using namespace std;

class Point {
private:
    double x;
    double y;

public:
    // default constructor
    Point() : x(0), y(0) { }

    // copy constructor
    Point(const Point& other) : x(other.x), y(other.y) { }

    Point(double a, double b) : x(a), y(b) { }

    // getters
    double get_x() const { return x; }
    double get_y() const { return y; }

    void print() const {
        cout << '(' << x << ", " << y << ')';
    }

    void println() const {
        print();
        cout << "\n";
    }
}; // class Point


class Person {
    string name;
    int age;
public:
    Person(const string& n, int a)
    : name{n}, age{a}
    {
        if (age < 0) cmpt::error("negative age");
    }

    string get_name() const { return name; }
    int get_age() const { return age; }

    void print() const {
        cout << "Name: '" << name << ", Age: " << age;
    }

    void println() const {
        print();
        cout << "\n";
    }
}; // class Person


class Reading {
private:
    string loc;
    double temp;
public:
    Reading(const string& l, double t)
    : loc{l}, temp{t}
    { }

    string get_loc() const { return loc; }
    double get_temp() const { return temp; }

    void print() const {
        cout << temp << " degrees at " << loc;
    }

    void println() const {
        print();
        cout << "\n";
    }
}; // class Reading

int main() {
    Point a{1, 2};
    a.println();

    Person b{"Katja", 22};
    b.println();

    Reading c{"backyard", 2.4};
    c.println();
}

Using Multiple Classes

notice that the println method is identical in Point, Person, and Reading

repeated methods are often a flag that simplification is possible

so what we can do is create a class called Printable that lets us inherit println automatically

class Printable {
public:

    // prints the object to cout followed by "\n"
    void println() const {
        print();
        cout << "\n";
    }

}; // class Printable

now we can re-write Point like this (we’ll only look at Point for the moment — the details are the same for the other two classes)

class Point : public Printable {
private:
    double x;
    double y;

public:
    // default constructor
    Point() : x(0), y(0) { }

    // copy constructor
    Point(const Point& other) : x(other.x), y(other.y) { }

    Point(double a, double b) : x(a), y(b) { }

    // getters
    double get_x() const { return x; }
    double get_y() const { return y; }

    void print() const {
        cout << '(' << x << ", " << y << ')';
    }

}; // class Point

Point is the same as before, except now it has : public Printable at the top, and println is no longer in the body

Inheritance

the idea here is that println is inherited by Point, meaning it is automatically added to Point

but there’s a problem: this example does not compile!

more specifically, the error you’ll get is that print is not declared inside println

class Printable {
public:
    void println() const {
        print();      //  <---- error on this line: print() is not defined
        cout << "\n";
    }

}; // class Printable

while the Point class does indeed define print(), that’s not soon enough for Printable

so we need to add a print() method to Printable

but, unlike println(), print() will be different for every class

so what should print() do inside Printable?

we don’t know!

so, C++ lets us define print without a body like this:

class Printable {
public:
    // prints the object to cout
    virtual void print() const = 0;

    // prints the object to cout followed by "\n"
    void println() const {
        print();
        cout << "\n";
    }

}; // class Printable

the = 0 at the end of print means that it has no body

the keyword virtual means that classes that inherit from Printable can supply a body

since there is no virtual in front of println(), classes that inherit from Printable cannot change println — they will inherit the version of println defined here in Printable

Inheritance and Destructors

with the addition of print to Printable, the code now compiles and runs!

but there’s still a serious problem

to see it, lets add a destructor to Point

class Point : public Printable {
private:
    // ...

public:
    // ...

    ~Point() {
        print();
        cout << " destroyed\n";
    }

    // ...
}; // class Point

this code works as expected:

Point a{1, 2};
a.println();

it prints this:

(1, 2)
(1, 2) destroyed

this code also works as expected:

Point* p = new Point(3, 4);
p->println();
delete p;

it prints this:

(3, 4)
(3, 4) destroyed

however, it should be noted that this code does not compile using our course makefile, because it generates a warning, and for us warnings are treated as fatal errors

even though it appears to work, the fact that we get a warning for this code is a red flag that there is a potential problem

but something different happens here:

Printable* p = new Point(3, 4);
p->println();
delete p;

do you see the difference?

p is of type Printable*, not Point* — everything else is the same

this compiles (with warnings!), and it prints this:

(3, 4)

apparently the destructor is not being called!

that’s a big problem — destructors are often vital, e.g. think of the int_vec class where the destructor is responsible for deleting the internal array (which avoids a memory leak)

Inheritance and Destructors

to see what’s happening, lets add a destructor to Printable

class Printable {
public:
    // prints the object to cout
    virtual void print() const = 0;

    // prints the object to cout followed by "\n"
    void println() const {
        print();
        cout << "\n";
    }

    ~Printable() {
        cout << "Printable destructor called\n";
    }

}; // class Printable

now run this code again:

Printable* p = new Point(3, 4);
p->println();
delete p;

it prints this:

Printable destructor called
(3, 4)

so the Printable destructor is called, but not the Print destructor

and it is pretty weird that the destructor for Printable is called before p->print()?!

if you read the warning message we are ignoring, it says the we are doing something that will result in undefined behaviour

warning: deleting object of abstract class type ‘Printable’ which has
non-virtual destructor will cause undefined behaviour
[-Wdelete-non-virtual-dtor]
  delete p;

Inheritance and Destructors

the solution to these problems is to use a virtual destructor inside Printable

class Printable {
public:
    // prints the object to cout
    virtual void print() const = 0;

    // prints the object to cout followed by "\n"
    void println() const {
        print();
        cout << "\n";
    }

    // virtual destructor
    virtual ~Printable() { }

}; // class Printable

the keyword virtual means that classes that extend Printable are allowed to over-ride the default destructor from Printable and substitute it with their own destructor

now this code compiles without warning:

Printable* p = new Point(3, 4);
p->println();
delete p;

it prints this:

(3, 4)
(3, 4) destroyed

which is exactly what we want

Virtual Destructors

in this course, we’ll always include virtual destructors in a base class (i.e. a class that will be inherited from) so that derived classes can, if they choose to, supply their own destructors

to help with this in g++, we will always compile with the -Wnon-virtual- dtor option turned on so that g++ will usually recognize situations where we haven’t done this

Some Terminology

here is the Printable class we have settled on

class Printable {
public:
    // prints the object to cout
    virtual void print() const = 0;

    // prints the object to cout followed by "\n"
    void println() const {
        print();
        cout << "\n";
    }

    // virtual destructor
    virtual ~Printable() { }

}; // class Printable

Printable is an abstract class because not all of its methods are implemented, i.e. the print method has no body

  • = 0 indicates a method is abstract; such methods are also known as pure virtual methods
  • a class is considered abstract if it has one, or more, abstract methods
    • it’s possible to for an abstract class to have one, or more, methods that are not = 0

also, we call Printable a base class, because its main use is to have other classes derive from it

for example, the Point class is said to derive from Printable

class Point : public Printable {
    // ...
};

we say that Point is derived from Printable

or that Point inherits from Printable

or that Point is a subclass of Printable

or that Point extends Printable

or that Point is a child of Printable

we can also say that Printable is a superclass of Point, or that Printable is a parent class of Point

the keyword public in the class header means that all the public methods from Printable will also be public when they are inherited in Point

C++ allows so-called multiple-inheritance, where a class can extend more than one class

single-inheritance means the class has exactly one parent class

in this course, we’ll usually restrict ourselves to single-inheritance, and always use public inheritance

Virtual Methods

inside Printable, the method print is declared virtual

virtual methods are an important concept in object-oriented programming

basically, if you declare a method in a class virtual then that means that derived classes can, if they choose to, supply their implementation for that method

if you look at Point, Person, and Reading, that’s what we did — each of those classes provides its implementation for the print method

The Idea of Binding

consider a regular function that is not inside any class or struct

void hello() {          // hello is bound to its body at compile-time
    cout << "Hello!\n";
}

when C++ compiles this function, the name hello gets bound to (associated with) the function body at compile-time

when you call hello(), the compiler knows exactly what code will be executed

when a name is bound at compile time, that’s called static binding

The Idea of Binding

a virtual method, such as print in Printable is not necessarily bound at compile time

i.e. it is possible that, at compile-time, there is no way for the compiler to know exactly which print() method is being called

for example:

Printable* p;
string s;
cin >> s;
if (s == "point") {
    p = new Point{2, 3};
} else {
    p = new Person{"Jill", 93};
}

p.print();  // Which print is called? Point's or Person's?

there is no way for the compiler to know exactly which print is being called in p.print() — it depends on what the user types

it doesn’t know which print() to call until run-time, i.e. when the program actually runs

in this case, we say that print is dynamically bound, i.e. print is associated with its body at run-time instead of compile-time

dynamic binding is one of the key features of object-oriented programming

Things to Note

class Point : public Printable {
    // ...
}

the keyword public here means that all the public methods from Printable will also be public when they are inherited in Point

Point extends only one class, i.e. Printable

C++ allows so-called multiple-inheritance, where a class can extend more than one class

  • Careful: while multiple inheritance does have its uses, it is more complicated to use especially in the case when the two (or more) classes you are inheriting from have an implemented method with the same name — which of the two implementations should be used in the inheriting class?
    • one popular solution to this problem with multiple inheritance is to simply not allow it, e.g. this is what languages such as Java and Ruby do
    • also, there is no problem with multiple inheritance if the classes you are inheriting from are abstract classes, i.e. none of the methods have implementations

single-inheritance means the class has exactly one parent class

in this course, we’ll restrict ourselves to single-inheritance, and always use public inheritance

Printable Objects

it’s impossible to make just a Printable object

Printable p; // compiler error: print() not implemented

however, pointers of type Printable* can be created, and they are very useful

Pointers to Printable Objects

Printable* p1 = new Point{1, 2};
Printable* p2 = new Person{"Max", 2};
Printable* p3 = new Reading{"Black Rock", 41.5};

p1->println(); // (1, 2)
p2->println(); // Name: 'Max, Age: 2
p3->println(); // 41.5 degrees at Black Rock

p1, p2, and p3 are all of type Printable*

but the objects they point to are all different types

Pointers to Printable Objects

suppose p is of type Pointer* and points to either a Point, Person, or Reading object (we don’t know which)

what does p->print() do?

there’s no way to know without knowing the type of the object being pointed to

we can’t tell from p->print() alone what gets printed!

A vector of Printable Objects

the elements of a C++ vector (or array) must all be of the same type

so we cannot have vector containing a Point, Person, and Reading object because they are all different types

but we can have a vector of Pointer* objects …

A vector of Printable Objects

vector<Printable*> v = { new Point{1, 2},
                         new Person{"Max", 2},
                         new Reading{"Black Rock", 41.5}
                       };
for(Printable* p : v) {
    p->print();
    cout << "\n";
}

//... be sure to delete v's elements ...

each time p->println() is called, a different version of print() is called

C++ generally cannot determine which print() is called until run-time

Pointers to Printable Objects

suppose p is of type Printable* and pointing to an object

we have no idea exactly what the exact type of the object p is pointing to

so we can only call methods through p that are declared in the Printable class

all objects that p points to, regardless of their exact type, are guaranteed to have print() and println()

so p->print() and p->println() are the only methods we can safely call through p

Pointers to Printable Objects

Reading* r = new Reading{"Black Rock", 41.5};
r->println();         // okay
cout << r->get_temp() // okay
     << "\n";

Printable* p = r;      // okay
p->println();          // okay
cout << p->get_temp()  // compile-time error!
     << "\n"; !

C++ and OOP

C++ was the first popular mainstream language to support OOP

earlier languages used it (e.g. Simula 67, SmallTalk), but have never been as popular

most new languages support OOP

most of them have somewhat simplified syntax compared to C++

e.g. Java renames -> to . and supports interfaces, which are like C++ classes with all the methods virtual and not implemented (= 0)

C++ and OOP

we are mainly going to use inheritance in this course in the style presented in these note, i.e. base classes of mostly virtual methods

that is, as a way specify what methods must be in an object

this is quite useful in practice

some programmers even think this is the best use of inheritance (and even that you should never inherit implemented methods like println())

be aware there are other styles and uses of inheritance in C++