Polymorphism in C++ Tom Latham

advertisement
Polymorphism in C++
Tom Latham
Recapping Day 4: Containers, Algorithms, Iterators, Lambdas
• C++ Standard Library provides:
• Generic {associative, sequence} containers, e.g. map and vector
• Iterators to unify method of access to container elements
• Algorithms to perform many generic tasks on containers, e.g. copy_if
• We also saw that lambdas are:
• A way to define a (perhaps single use) function inline with where it is used
• A powerful tool to use with the algorithms to enhance their capabilities
• Used containers and algorithms (with std functions and lambdas) to
implement the Playfair cipher
Today: From Static to Dynamic Polymorphism
• “Polymorphism”
• The ability of one generic type to behave in many concrete ways.
• We have already seen this behaviour with the container classes
• Enabled through the use of templates
• The polymorphism is locked at compile time
• Hence this is referred to as static polymorphism
• Today we’ll see how to achieve this at run time: dynamic polymorphism
• This is achieved through type-sharing
• In C++, the mechanism for this is public inheritance.
Templated functions
• Consider the two swap functions
on the right - we can see that the
only difference between them is
the type (of the arguments and of
the tmp variable)
• We are duplicating code – and
you can see that this would
proliferate if we wanted to have
other functions to swap two
float’s or two bool’s
• This duplication can be eliminated
using templates
void swap( double& a, double& b )
{
double tmp {b};
b = a;
a = tmp;
}
void swap( int& a, int& b )
{
int tmp {b};
b = a;
a = tmp;
}
Static polymorphism
• This function template definition
specifies a family of functions that swap
the values of two variables
• We have a single bit of code that can
act in different ways depending on the
type of T – it behaves polymorphically
• However, this difference is locked-in at
compile time with the specification of
the template parameter, either explicitly
or by compiler deduction
• In this particular case, the compiler can
deduce the template parameter from the
type of the arguments
template <typename T>
void swap( T& a, T& b )
{
T tmp {b};
b = a;
a = tmp;
}
int main()
{
double x {42.3};
double y {11.2};
swap(x,y);
• In some scenarios that isn’t possible, and so
it has to be specified explicitly:
int i {4};
int j {-6};
swap<double>(x,y);
• This can also be done even when the
compiler can deduce the type, e.g. to
provide clarity
}
swap(i,j);
Types
• On the first day we defined four important concepts, including type :
• A type defines the set of possible values and a set of operations for
an object.
• An object is some memory that holds a value of some type.
• A value is a set of bits interpreted according to a type.
• A variable is a named object.
• We've also seen that that there are:
• Built-in types: int, double, char, …
• User-defined types: CaesarCipher, PlayfairCipher, …
• Families of types: vector<int>, vector<double>,
vector<char>, …
Sharing Type
• Software design often throws up cases where a set of types all exhibit an
Is a Kind Of relationship to some, more abstract, concept.
• e.g. Car, Bus, Lorry are all kinds of vehicle
• In this example, they all have engines that can be started and stopped, they
can be travelling at a given speed, in a certain gear, etc. etc.
• What this means in programming terms is that they all provide the same
interface, i.e. the same set of public member functions, e.g.
• int changeGear();
• bool startEngine();
Sharing Type in a Strongly Typed Language
• We might want to store instances of, say, Car, Motorbike, Scooter in a container
vector<??> caughtSpeeding;
• We might design a class that has a data member that may change between, say, Car, Motorbike,
Scooter as time changes:
class Employee {
private:
?? personalVehicle;
};
• Unfortunately, neither is possible directly as C++ is a “strongly typed” language, i.e. we have to
supply, at compile-time, the type “??”.
• We need a mechanism to express the type sharing relationship in the code.
Sharing types in mpags-cipher
• By now you should have implemented several different ciphers as classes:
• CaesarCipher
• VigenereCipher
• PlayfairCipher
• These are all kinds of cipher and should have the same interface: encrypt and
decrypt functions
• We want to be able to express this relationship in our code to avoid
duplication of the code where we used the different ciphers
Public Inheritance
• Type sharing in C++ is achieved through public inheritance.
• You create a new class that contains only the declarations of the member
functions, i.e. the interface, that defines the common, abstract type.
• This class generally contains no data members and has no
implementation code!
• So you generally only need a header (.hpp) file for this base class
• Classes that want to share this type publicly inherit from this base class.
• What they inherit is the obligation to provide the public member functions
specified by the base class.
Writing Purely Abstract Base Classes
A pABC specifies the interface a type must provide and implement, but no data and no
implementation for this interface, as shown below.
We see the new C++ keyword virtual prepended to each method signature. This tells the
compiler to defer the decision on which actual code to call until runtime (dynamic binding).
Each method has =0 appended to inform the compiler that no actual implementation is provided
for the method - it is pure virtual .
Notes
It's important to note that the
destructor must be declared and
must be virtual. This is so that
classes inheriting the pABC are
destructed correctly.
This has the side effect that all the
other "special functions" must
also be declared.
The "= default" syntax indicates
that the compiler-supplied
versions should be used (we
don't have to write the code
ourselves) – see next slide for
details.
Aside: Compiler-provided functions
• So what are these "special functions" that we saw on the previous slide?
And why have we not considered them before?
• They all handle resource management and are called:
• Copy constructor
• Move constructor
• Copy assignment operator
• Move assignment operator
• Destructor
• If you do not specify them, the compiler creates them for you
• In the majority of cases the compiler-provided versions will work perfectly
well, which is why we have not had to worry about them so far!
Aside: Compiler-provided functions
• So what are these "special functions" that we saw on the previous slide?
And why have we not considered them before?
• They all handle resource management and are called:
• Copy constructor
• Move constructor
• Copy assignment operator
• Move assignment operator
Allows objects to be
created by copying an
existing instance
• Destructor
• If you do not specify them, the compiler creates them for you
• In the majority of cases the compiler-provided versions will work perfectly
well, which is why we have not had to worry about them so far!
Aside: Compiler-provided functions
• So what are these "special functions" that we saw on the previous slide?
And why have we not considered them before?
• They all handle resource management and are called:
• Copy constructor
• Move constructor
• Copy assignment operator
• Move assignment operator
Allows objects to be
created by moving an
existing instance
• Destructor
• If you do not specify them, the compiler creates them for you
• In the majority of cases the compiler-provided versions will work perfectly
well, which is why we have not had to worry about them so far!
Aside: Compiler-provided functions
• So what are these "special functions" that we saw on the previous slide?
And why have we not considered them before?
• They all handle resource management and are called:
• Copy constructor
• Move constructor
• Copy assignment operator
• Move assignment operator
Allows objects to be
assigned to by copying
an existing instance
• Destructor
• If you do not specify them, the compiler creates them for you
• In the majority of cases the compiler-provided versions will work perfectly
well, which is why we have not had to worry about them so far!
Aside: Compiler-provided functions
• So what are these "special functions" that we saw on the previous slide?
And why have we not considered them before?
• They all handle resource management and are called:
• Copy constructor
• Move constructor
• Copy assignment operator
• Move assignment operator
Allows objects to be
assigned to by moving
an existing instance
• Destructor
• If you do not specify them, the compiler creates them for you
• In the majority of cases the compiler-provided versions will work perfectly
well, which is why we have not had to worry about them so far!
Aside: Compiler-provided functions
• So what are these "special functions" that we saw on the previous slide?
And why have we not considered them before?
• They all handle resource management and are called:
• Copy constructor
• Move constructor
• Copy assignment operator
• Move assignment operator
Cleans up any
allocated resources
when the object is
destroyed
• Destructor
• If you do not specify them, the compiler creates them for you
• In the majority of cases the compiler-provided versions will work perfectly
well, which is why we have not had to worry about them so far!
Aside: Compiler-provided functions
• However, in some cases there is the need to write custom versions
• Or, as we've done here, the compiler-provided destructor is fine but we need to
specify that it should be virtual
• The problem is that if you specify one of these functions, the compiler assumes that
you are taking over (to some extent or other) and will not provide some or all of
these five functions
• So we have to say explicitly that we want the compiler to continue to provide these
for us, which is what the "= default" does
• For more details see:
http://en.cppreference.com/w/cpp/language/rule_of_three
And links therein.
• We would generally advise to follow the "rule of all or nothing", i.e. to specify none
of them when possible (the "rule of zero") but if you have to specify one you should
do so for all five (the "rule of five")
Exercise: A pABC for a cipher interface
• Write a pABC called Cipher
• It should have two pure virtual member functions:
• The functions to encrypt and decrypt the supplied text:
virtual std::string encrypt( const std::string& input ) const = 0;
virtual std::string decrypt( const std::string& input ) const = 0;
• It will also need the lines shown on previous slide to obtain the virtual
destructor (and keep all the other compiler-defined special functions)
• Make sure to add the appropriate documentation comments
Recap on Public Inheritance
• It is the mechanism by which we inform the compiler that our concrete
classes - those from which we create object instances - also hold the type of
the abstract base class
• The terms derived class and base class are often used to describe this
inheritance relationship
• Public inheritance means that all the public methods in the base class remain
public in the derived class (hence they share an interface).
• C++ also allows private and protected inheritance, which result in sharing
of implementation but not shared type
• Composition/Aggregation are better design patterns in these cases.
Writing Derived Classes
To derive a class, say Car, from our Vehicle base class, we append ": public Vehicle"
(as shown below) to the class declaration, to make Car publicly inherit from Vehicle.
The Car interface must include all the pure virtual function signatures from Vehicle, plus any
functions specific to Car (like the constructor) and data members relevant to Car. The
implementation of the virtual functions for Car follows just as for any other class.
Notes
Like any other type, we
have to ensure the
declaration of the base
class is present before we
can inherit from it.
As the derived class will
implement the virtual
functions, these are still
declared virtual in Car, but
we replace the =0 marker
with override.
Exercise: Deriving from Cipher
• Now you can modify your concrete classes (CaesarCipher,
VigenereCipher, PlayfairCipher) so that they inherit from your new
base class (Cipher)
• Depending on your exact implementation of the concrete classes, it may be
as simple as adding ": public Cipher" to the class declaration and
adding override to the end of the encrypt and decrypt function declarations
• Or it might mean some small modifications to function names / argument
types etc.
• If it looks like you'll have to make major changes then talk to one of us!
Dynamic Polymorphism
• We've now got implementations of three concrete types, CaesarCipher,
VigenereCipher, PlayfairCipher, which also have the shared type
Cipher
• Now we can take advantage of the combination of
• Type sharing (via public inheritance)
• Dynamic binding (via virtual member functions)
• So we can now address a set of objects that are instances of different
derived classes through, e.g. a reference to an object of the base class type
Using Dynamic Polymorphism
We can now write functions that have a reference to an object of the base class type as an
argument. As we can see below, via dynamic polymorphism, we can supply this function with
instances of the different derived classes because they share type with the base class.
Each time we call the function, we can get different behaviour depending on the
concrete type of the instance provided.
Notes
We can only achieve
dynamic polymorphism via
references or (smart)
pointers (see later slides).
Dynamic polymorphism in mpags-cipher
• How can we be taking advantage of dynamic polymorphism in our program?
• At the moment we have a set of if…else if…else's in which we construct the
concrete instances and use them to either encrypt or decrypt
• Would be cleaner and far more reusable to have a 'factory' function that
constructs an object instance (the concrete type of which depends on a
supplied argument) and returns it to us (using the base type)
• For our test suite it could also be advantageous to store various ciphers in a
container to be able to loop through them
• Can we do these things with references?
Limitations of references
• For the factory function there is the problem of object lifetime and reference returns
(alluded to when we first introduced references in Day 2)
• But also we cannot use references in containers since they cannot be copied or
assigned to – these are prerequisites for any object being stored in a container (in
fact references are not objects but provide an alias for an object that already exists)
• (Since C++11 a reference_wrapper class is available to allow storage of
references in containers but we will not examine that further here)
• In addition to what we mentioned on the previous slide, we also might like to have
one of our polymorphic types as a data member of a class
• But reference data members can only be initialised in the constructor and then
they can never be modified to refer to another object, which is rather limiting
Smart pointers
• The solution to these various problems is to be able to do dynamic allocation and to
manage the associated memory and ownership issues using smart pointers
• Smart pointers are objects that point to other objects, in particular to objects that
have been dynamically allocated
• What is dynamic allocation? Essentially it means to create objects in an area of
memory (called the free store) that gives them a lifetime beyond the scope in which
the allocation takes place.
• Prior to C++11 the allocation and management was dealt with manually (using new
and delete and raw pointers – more on these next week)
• Since C++11 we have the smart pointers and their helper functions to handle all of
that for us – makes for much safer code!
Smart pointers
• There are three types of smart pointer provided in the C++ standard:
• unique_ptr
• shared_ptr
• weak_ptr
• We will discuss the first two of these (the last is only useful in a handful of situations)
• Smart pointers ensure destruction of the managed object at the appropriate time:
• The unique_ptr is used to enforce sole-ownership of an object. So when the
that one unique_ptr is destroyed (or assigned a new object to manage) it
triggers the destruction of the managed object.
• The shared_ptr is used for shared resources, where there is no one owner. So
when the reference count (i.e. the number of shared_ptr's referring to this
same managed object) falls to zero the managed object is destroyed.
Dynamic allocation with std::make_unique
We want to make a factory function that can construct our objects with dynamic storage
duration and return us sole ownership of those objects. Thus we want to use
std::unique_ptr and its helper function std::make_unique.
The return type is a std::unique_ptr to the base type, in this case Vehicle.
The std::unique_ptr's to the concrete types are implicitly converted on the return.
Notes
We select what particular
concrete type we want using
an enumeration.
Arguments to the
std::make_unique
function are forwarded on as
arguments to the constructor.
We need C++14 to use
std::make_unique – it was
accidentally omitted from the
C++11 standard!
Exercise: A Factory Function
• Write a factory function for your cipher classes
• You can use the example on the previous slide as a guide
• Then modify your main code to use this new function to create your cipher
object depending on the corresponding command line option, e.g.
auto aVehicle = vehicleFactory( VehicleType::CAR, nGears );
• You can then use the unique_ptr with the arrow operator "->" instead of
the dot operator ".", e.g.
double speed { aVehicle->currentSpeed() };
Collections of polymorphic types
• We can also use the unique_ptr<Base> as the type to store in collections
• For example, an inventory of vehicles:
std::vector<std::unique_ptr<Vehicle>> inventory;
inventory.push_back( vehicleFactory( VehicleTypes::LORRY, nGears ) );
...
for ( const auto& v : inventory ) {
std::cout << v->numberOfGears() << "\n";
}
Exercise: Test all ciphers using a collection
• Write some new tests for your ciphers
• Create a collection of ciphers and fill it with one of each type, using your
factory function
• Loop through the collection and check that each is encrypting as expected
• Similarly, write another test to check that all are decrypting as expected
• Again, you can use the example on the previous slide as a guide
When to use shared_ptr?
• A shared_ptr is used to express a shared ownership of some resource
• More than one client needs to use the resource and it is not obvious that a
particular one of them should be the single owner
• For example, an employee has a company car but there is also an inventory
of all company vehicles, another list of those that are under a particular
service contract, etc.
• This is a case where the resource should be managed by a shared_ptr
• (There is potentially a case for some of those clients to hold weak_ptr's,
which track the object but don't hold a share of the ownership. But this is
probably quite rare.)
BACKUP
Abstract Base Classes
• In some cases you can find that you have a lot of duplicated code in (some
of) the concrete classes (not an issue for these cipher classes)
• We could move some of this code up into the pABC but then it wouldn’t be
purely abstract any more and its job is to simply define a type
• So instead we add a new layer in the inheritance structure, an Abstract Base
Class (or ABC), which inherits from the pABC and from which (some of) the
concrete classes inherit (instead of from the pABC)
• So we have the pABC that defines the type and the ABC that allows some
sharing of implementation
• For example, we could have a MotorVehicle ABC from which Car, Lorry, etc.
inherit but Bicycle does not
Protected access
• This is the third category of access specifier (public, protected, private)
• It is useful in any scenario where you have a utility function in an ABC that
needs to be called by the derived classes
• You don’t want it to be public but making it private hides it from the
derived classes
• Making it protected means that it can be accessed by the ABC itself and
any classes that are derived from it
Download