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
= 0indicates 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
- it’s possible to for an abstract class to have one, or more, methods that
are not
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++