Today's lab session covers class inheritance and the use of abstract classes. Of special importance are the details concerning creation, destruction and copying of objects within class hierarchies. Along the way you will learn a small & handy C++11 feature. The main exercise builds upon the function-plotter exercise you did last week.
Designing class hierarchies by deriving concrete classes from more abstract ones is one of the most essential features of object-oriented programming languages; also C++.
A point worth noting is that the concept of deriving classes predates the introduction of C++ by roughly 15 years.
By no means is C++ a purely object-oriented language, nor is it the most elegant implementation of the principles.
Being a very popular general purpose language, C++ has many of the features that allow for an object-oriented approach of designs.
Keep in mind though, that there are many other languages that are "more object-oriented" than C++.
Do some reading if you're interested in some more details.
Since the lab sessions are all about practical examples, we'll jump right into the syntax of deriving a class Derived
from another class Base
:
class Base {
public:
void foo();
};
class Derived : public Base {
public:
void bar();
};
In this simple case, the UML diagram (generated from the above code snippet with Doxygen by the way) looks like:
Objects of the
Derived
class have two members: foo
(derived from Base
) and bar
(defined in Derived
).
Therefore, the following is valid:
Derived d;
d.foo();
d.bar();
More generally: a member of a derived class can use the public
and protected
members of its base class as if they were declared in the derived class.
You can say that a derived class is "larger" than its base class in the sense that it holds more data and provides more functions.
Note the public
inheritance.
This means that all access modifiers (public
, private
and protected
) of the Base
class members stay the same in Derived
.
This is the most common scenario we'll encounter.
Two other possibilities are:
protected
: public members of Base
become protected in Derived
private
: all members from Base
become private in Derived
If Base
has constructors, they must be invoked in Derived
.
This invocation can be implicit (i.e., automatic) in the case of a default constructor.
On the other hand, if Base
's constructor needs arguments, it must be called explicitly.
This can be summarized as: constructors are never inherited.
Also, you can't directly initialize members of Base
in the constructor of Derived
; even if they're not private (assignment works though).
Base.h
:
class Base {
public:
Base(int a);
};
Base.cpp
:
Base::Base(int a) {
...
}
Derived.h
:
class Derived : public Base {
public:
Derived(int a, int b);
};
Derived.cpp
:
Derived::Derived(int a, int b)
: Base(a) { // <- EXPLICIT call to Base's non-default constructor
...
}
The order in which objects are created can be summarized as:
These rules are really very important. One part of the final exam is performing a trace where you explicitly write out the order of construction & destruction operations on objects. Make sure you know it all!
The above is not 100% true anymore. Please learn about constructor inheritance here and here to see what's been added to C++11 to ease object creation for inherited classes.
Suppose the following scenario:
Base.h
:
class Base {
public:
Base(int a);
};
Base.cpp
:
Base::Base(int a) {
cout << "Base constructor, a = " << a << endl;
}
Derived.h
:
class Derived : public Base {
};
main.cpp
:
// -- SNIP --
Derived d(1);
// -- SNAP --
My g++
fails with error: no matching function for call to 'Derived::Derived(int)'
.
C++11 provides a way to fix this.
Read the above Wikipedia article and make the example work!
In both articles, another interesting C++11 feature is discussed: constructor delegation.
Which part of the following snippet relate to constructor inheritance and which part to constructor delegation?
class Base {
public:
Base(int n) : number(n) {};
private:
int number;
};
class Derived: public Base {
public:
using Base::Base;
Derived() : Derived(5) {};
};
Suppose you're doing this:
Derived d;
Base b = d;
or an assignment like:
Derived d;
Base b;
b = d;
The result is that b
only knows
about Base
properties of d
.
All the rest is not copied. This is called slicing.
There's an interesting consequence of how constructors are handled in class hierarchies when you're making copies of objects.
Consider:
Base.h
:
class Base {
public:
Base();
Base(const Base&);
};
Base.cpp
:
Base::Base() {
cout << "Base::Base()" << endl;
}
Base::Base(const Base&) {
cout << "Base::Base(const Base&)" << endl;
}
Derived.h
:
class Derived : public Base {
public:
Derived();
Derived(const Derived&);
};
Derived.cpp
:
Derived::Derived() {
cout << "Derived::Derived()" << endl;
}
Derived::Derived(const Derived&) {
cout << "Derived::Derived(const Derived&)" << endl;
}
What's the output when I do:
Derived d;
Derived d_copy(d);
Do you think what you see is the expected behaviour?
If not, what's wrong, and how should you fix it?
Check if the assignment operator operator=
is similar in how the above situation is handled.
Contrary to the above, when passing around pointers or references to objects, no copies are involved, nor is there any slicing (i.e., no information is lost). For example:
Derived* d_ptr = new Derived;
Base* b_ptr = d_ptr;
Now b_ptr
pretends to be a pointer to Base
but is actually pointing to a Derived
object.
This brings us to the important subject of virtual functions.
Referring to Derived
objects through a pointer / reference to Base
allows using polymorphic method calls. Try this example:
Base.h
:
class Base {
public:
virtual void print_info();
};
Base.cpp
:
void Base::print_info() {
cout << "Base::print_info()" << endl;
}
Derived.h
:
class Derived : public Base {
public:
virtual void print_info(); // NOTE: 'virtual' is optional here, 'override' might be better.
};
Derived.cpp
:
void Derived::print_info() {
cout << "Derived::print_info()" << endl;
}
Note that the keyword virtual
only appears in the class definition (.h
file); NOT in the definition of its methods (.cpp
file)!
Now, different print_info
methods will be called depending on object's type:
Base* b_ptr = new Base;
Derived* d_ptr = new Derived;
Base* b_ptr2 = new Derived;
Base& b_ref = *d_ptr;
b_ptr->print_info(); // Base::print_info()
d_ptr->print_info(); // Derived::print_info()
b_ptr2->print_info(); // Derived::print_info() !!!
b_ref.print_info(); // Derived::print_info() !!!
In all cases the object's dynamic type determines which method is called.
The dynamic type is the type of the object pointed to at runtime (remember that a pointer to a Base
object can actually point to a Derived
object).
The C++ FAQ lite has a whole section on virtual functions. Especially the part on how virtual functions work internally is interesting if you want a deeper knowledge of C++!
This is called polymorphism. In C++ it only works with pointers and references to objects. Try removing the virtual
keyword from the above example. What happens?
A slight downside to virtual functions as defined above is that you are required to implement all methods, even if they really don't do anything useful yet, as might be the case in the Base
class.
More often than not a base class specifies how derived classes should look like (i.e., the interface).
The consequence is that many of the base class methods end up having an empty body.
class TheAnswer {
public:
virtual int get() {
// The base class represents a concept that is too general to
// decide what exectly should be returned. However, the get()
// method needs to be implemented or the code won't compile.
// If you turn on all warnings in gcc with -Wall, you'll even
// get:
// warning: no return statement in function returning non-void
// but you don't know what to return!
// Surely there must be a better way to handle this...
}
};
class TheAnswerToLifeTheUniverseAndEverything : public TheAnswer {
public:
virtual int get() {
return 42;
}
};
The solution to the above problem is declaring TheAnswer::get()
to be a pure virtual function.
A pure virtual function has no definition, only a zero-initializer:
class TheAnswer {
public:
// We don't need to make up answers just to please the compiler!
virtual int get() = 0;
}
As you might have guessed, it now doesn't make any sense to create objects of the type TheAnswer
since they don't know how to behave if you call their get()
method.
Any attempt to compile this:
TheAnswer a;
Indeed gives:
error: cannot declare variable 'a' to be of abstract type 'TheAnswer'
note: because the following virtual functions are pure within 'TheAnswer':
note: virtual int TheAnswer::get() = 0;
As GCC's error message hints, we'll call TheAnswer
an abstract base class.
Read C++ FAQ's section on abstract base classes now.
(often abbreviated as RTTI)
In the context of class hierarchies three different types of casts can be performed between pointers (or references) to objects of different types:
Derived*
to Base*
Base*
to Derived*
Base1*
to a Base2
(only for multiple inheritance)"Whatever points to an object of the Derived
class, must also point to a Base
":
Derived* d_ptr = new Derived;
Base* b_ptr = d_ptr;
"If you've got a pointer to a Base
object, you can't assume it's pointing to a Derived
object".
Unless you know for sure, in which case you can use a dynamic_cast
:
Base* b_ptr = new Derived;
Derived* d_ptr = dynamic_cast<Derived*>(b_ptr);
Note that the use of dynamic_cast
is often frowned upon, as this usually means you applied polymorphism incorrectly.
Look at the concept of duck typing and the Liskov substitution principle for when to correctly apply inheritance.
An alternative to downcasting can be a double dispatch, potentially in combination with the visitor pattern.
Also, have a look at some discussions about the use of dynamic_cast
, for instance here or here.
Now, try to remove the virtual function from the Base class. What happens?
Google exercise: look it up!
RTTI deals with the case in which the correctness of the type conversion cannot be determined by the compiler at compile-time and must be postponed until run-time.
The primary purpose of the dynamic_cast
operator is to perform type-safe downcasts. If the conversion fails, a null pointer is returned.
In the case of references, a bad_cast
exception is thrown.
You can use it for checking the exact type of a polymorphic object:
// Both Derived1 and Derived2 are subclasses of Base
void f(Base* b_ptr) {
if (Derived1* d1_ptr = dynamic_cast<Derived1*>(b_ptr)) {
d1_ptr->derived1Method();
} else if (Derived2* d2_ptr = dynamic_cast<Derived2*>(b_ptr)) {
d2_ptr->derived2Method();
} else {
b_ptr->baseMethod();
}
}
Last week, you designed a Plotter
class for plotting general functions that evaluated a polynomial in an interval between a and b and printed those values. This week you'll create a concrete implementation of a plotter class that uses the SFML library for graphical output.
Start by subclassing Plotter
with a class PlotterSFML
. The constructor of PlotterSFML
should take care of setting up the graphical context. Its destructor should handle the clean-up.
The plot(const Polynomial &polynomial, double a, double b, unsigned int n)
method does all the work of evaluating the function f
in n
points equally spaced in the interval [a,b]
and plotting it on the screen.
You'll also need to perform a coordinate transformation to map the [a,b]
and [f(a),f(b)]
intervals on the available window width and height.
Make sure the Plotter
base class is abstract by declaring Plotter::plot(...)
pure virtual.
You will need to do some research on how to use SFML; this tutorial is a decent starting point.
For drawing the function f
as a set of subsequent points connected by lines I suggest you use sf::LinesStrip
, see here.
CMake does not know where or how to find SFML by default. You'll have to instruct it where to look for SFML. CMake comes bundled with a variety of Find<WhateverLibrary>.cmake
scripts that are used for discovering components required for compilation. Luckily, FindSFML.cmake
is included in the current CMake distribution, but for older versions you might have to manually include it inside a cmake/Modules
folder as part of your project.
You can instruct CMake to look for the SFML library using FIND_PACKAGE(SFML COMPONENTS system window graphics)
. Next, we need to make sure the compiler knows where to find the SFML header files that we include in our code, by using INCLUDE_DIRECTORIES(${SFML_INCLUDE_DIR})
.
Finally, CMake needs to instruct the linker to link our compiled binary to the SFML libraries, which is done using TARGET_LINK_LIBRARIES(plotter ${SFML_LIBRARIES})
.
Start with the following
CMAKE_MINIMUM_REQUIRED(VERSION 2.8.12)
PROJECT(PlottingPolynomials)
ADD_EXECUTABLE(plot src/Plotter.cpp src/Polynomial.cpp src/main.cpp)
And the code:
// Polynomial.h
#ifndef POLYNOMIAL_H
#define POLYNOMIAL_H
#include <vector>
#include <ostream>
class Polynomial {
public:
/**
* Creates a polynomial with coefficients from coeff.
* Coefficients are ordered from x^0 to x^n.
*/
Polynomial(std::vector<double> coeff);
/**
* Evaluates the polynomial in a point x using a naive method.
*/
double apply(double x) const;
/**
* Evaluates the polynomial in a point x using Horner's method.
*/
double eval_horner(double x) const;
/**
* Returns the first derivative of a polynomial as a new Polynomial.
*/
Polynomial derive() const;
/**
* Unary - operator.
*/
Polynomial operator-() const;
/**
* Binary + operator.
*/
Polynomial operator+(const Polynomial& that) const;
/**
* Binary - operator.
*/
Polynomial operator-(const Polynomial& that) const;
/**
* Binary * operator.
*/
Polynomial operator*(const Polynomial& that) const;
/**
* += operator
*/
Polynomial& operator+=(const Polynomial& that);
/**
* -= operator
*/
Polynomial& operator-=(const Polynomial& that);
/**
* *= operator
*/
Polynomial& operator*=(const Polynomial& that);
// Make sure ostream::operator<< has access to Polynomial's internals
friend std::ostream& operator<<(std::ostream& out, const Polynomial& p);
private:
std::vector<double> m_coeff; /// Polynomial's coefficients
};
#endif /* POLYNOMIAL_H */
// Polynomial.cpp
#include "Polynomial.h"
#include <iostream>
#include <iomanip> // For std::setw()
Polynomial::Polynomial(std::vector<double> coeff)
: m_coeff(coeff) {
}
double Polynomial::apply(double x) const {
double x_pow = 1.0;
double result = 0.0;
for (double a : this->m_coeff) {
result += a * x_pow;
x_pow *= x;
}
return result;
}
double Polynomial::eval_horner(double x) const {
double result = 0.0;
for (auto it = m_coeff.crbegin(); it != m_coeff.crend(); ++it) {
result *= x;
result += *it;
}
return result;
}
Polynomial Polynomial::derive() const {
std::vector<double> d_coeff(m_coeff);
unsigned int n = 0;
for (double& a : d_coeff) {
a *= (n++);
}
return Polynomial(d_coeff);
}
Polynomial Polynomial::operator-() const {
Polynomial result(*this);
for (double& a : result.m_coeff) {
a = -a;
}
return result;
}
Polynomial Polynomial::operator+(const Polynomial& that) const {
// Sizes of both polynomial's coefficient vectors
size_t this_n = this->m_coeff.size();
size_t that_n = that.m_coeff.size();
// a are the coefficients of the polynomial with the largest degree
const std::vector<double>& a = this_n > that_n ? this->m_coeff : that.m_coeff;
// b are the coefficients of the polynomial with the smallest degree
const std::vector<double>& b = this_n > that_n ? that.m_coeff : this->m_coeff;
// Create result with coefficients from a (higher degree poly.)
Polynomial result(a);
// ... and add up coefficients from b (smaller degree poly.)
for (size_t i = 0; i < b.size(); ++i) {
result.m_coeff.at(i) += b.at(i);
}
return result;
}
Polynomial Polynomial::operator-(const Polynomial& that) const {
return (*this) + (-that);
}
Polynomial Polynomial::operator*(const Polynomial& that) const {
size_t a_n = this->m_coeff.size();
size_t b_n = that.m_coeff.size();
Polynomial result(std::vector<double>(a_n + b_n - 1));
for (size_t i = 0; i < a_n; ++i) {
for (size_t j = 0; j < b_n; ++j) {
result.m_coeff.at(i + j) += this->m_coeff.at(i) * that.m_coeff.at(j);
}
}
return result;
}
Polynomial& Polynomial::operator+=(const Polynomial& that) {
*this = (*this) + that;
return *this;
}
Polynomial& Polynomial::operator-=(const Polynomial& that) {
*this = (*this) - that;
return *this;
}
Polynomial& Polynomial::operator*=(const Polynomial& that) {
*this = (*this) * that;
return *this;
}
std::ostream& operator<<(std::ostream& out, const Polynomial& p) {
out << "Polynomial coeff. (increasing order): ";
for (double a : p.m_coeff) {
out << std::setw(4) << a;
}
return out;
}
// Plotter.h
#ifndef PLOTTER_H
#define PLOTTER_H
#include <functional>
#include "Polynomial.h"
class Plotter {
public:
/**
* f: double -> double function
* a: left point of [a,b]
* b: right point of [a,b]
* n: number of points in [a,b]; including a & b
*/
static void plot(const Polynomial &polynomial, double a, double b, unsigned int n);
private:
};
#endif /* PLOTTER_H */
// Plotter.cpp
#include "Plotter.h"
#include "Polynomial.h"
#include <iostream>
#include <vector>
void Plotter::plot(const Polynomial &polynomial, double a, double b, unsigned int n) {
// The 1D grid to use for plotting & evaluation of f(x)
std::vector<double> x(n);
// Evaluated values of f in all grid points x
std::vector<double> fx(n);
// Distance between subsequent grid points
const double h = (b - a) / (n - 1);
size_t i = 0;
for (double& x_i : x) {
x_i = a + i * h;
fx.at(i) = polynomial.apply(x_i);
std::cout << x_i << " -> " << fx.at(i) << std::endl;
++i;
}
}
// main.cpp
#include "Polynomial.h"
#include "Plotter.h"
int main() {
Polynomial a{{3, 2, 1}};
Plotter::plot(a, -20, 20, 200);
}
The final CMakeLists.txt
file could look like this:
CMAKE_MINIMUM_REQUIRED(VERSION 2.8.12)
PROJECT(Plotter)
IF (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
SET (CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}/installed" CACHE PATH "default install path" FORCE)
ENDIF()
# Look for SFML >= 2.0, with the specific subcomponents listed below
FIND_PACKAGE(SFML 2 COMPONENTS system window graphics)
# The source files to compile
SET(SRC src/Plotter.cpp src/PlotterSFML.cpp src/Polynomial.cpp src/main.cpp)
# Add include dirs for SFML
INCLUDE_DIRECTORIES(${SFML_INCLUDE_DIR})
# The program we're compiling with the sources set above
ADD_EXECUTABLE(plotter ${SRC})
# Link target to SFML libs
TARGET_LINK_LIBRARIES(plotter sfml-system sfml-graphics sfml-window)
# Install our executable to the CMAKE_INSTALL_PREFIX/bin
INSTALL(TARGETS plotter DESTINATION bin)
Write a simple program to show why destructors should always be declared virtual in the context of derived classes.
Start from the following code:
#include <iostream>
class Base {
public:
Base(int value) : m_value(new int(value)) {}
~Base() {
std::cout << "Base::~Base called" << std::endl;
delete m_value;
}
private:
int *m_value;
};
class Derived : public Base {
public:
Derived(int base_value, int derived_value) : Base(base_value), m_derived_value(new int(derived_value)) {}
~Derived() {
std::cout << "Derived::~Derived called, ";
delete m_derived_value;
}
private:
int *m_derived_value;
};
int main() {
// your code here.
return 0;
}