Laboratory Training 3
Using Polymorphism and Templates
1 Training Tasks
1.1 The Hierarchy of Classes
Implement classes "Human," "Citizen", "Student", and "Employee". Each class must implement virtual function that shows related data on the screen. Create an array of pointers to different objects of the class hierarchy. Create a cycle and display data that represents objects of different types.
1.2 Using Polymorphism for Callback
Create a class for solving problem set in task 1.2 of the sixth laboratory training from previous semester. The class should contain at least two member functions: a function that returns some value according to individual task, and pure virtual function that is called from the previous one and defines the left side of the equation or researched function (according to the task).
Class should be allocated in a separate header file. The relevant implementation file should contain the definition of non-abstract member functions.
Another translation unit should contain a class derived from the previous one. This class should contain the
definition of the virtual function, which is a subject of investigation. Create an object of the derived class
and implement the individual task in main()
function.
Note: You should add some base class member functions that will calculate the first (second) derivative.
1.3 Using Templates for Callback
In a separate header file, create a template function for solving task 1.2 of the sixth laboratory training of the previous semester. The first parameter of the function must be an object of the template type to which the parentheses operation can be applied.
Check the operation of the template on two functions that are subject to research. One of the functions must be implemented as a functional object.
Note: To calculate the first (second) derivative, separate template functions should be added.
1.4 Class Template for Representation of Two-Dimensional Array
Convert class created in the task 1.4 of the previous laboratory training into class template. Implement global
template function that returns minimum array item. Create arrays of integers, real numbers, and simple fractions
(previously created class) in main()
function. For these three arrays you should test the function
of finding the minimum value, as well as other class features with catching possible exceptions. You should also
solve the problem from the individual task.
Note: in order to be able to find the minimum value in the array of fractions you should overload comparison operation for objects of the class "Simple fraction".
1.5 Library of Template Functions for Working with an Array (Additional Task)
Create a header file with functions that work with an array of an arbitrary generic type. The following functions should be implemented:
- exchange of places of elements with the specified indexes;
- search for an element with a certain value;
- exchange of places of all pairs of adjacent elements (with even and odd index).
Demonstrate the operation of all functions using at least three different types of data.
2 Instructions
2.1 Using UML to Represent Classes
In the Laboratory training # 1 of the previous semester Activity diagrams of the Unified Modeling Language (UML) were considered. The purpose of creating UML from the beginning was the graphical representation of classes. Class diagrams express the static structure of a system, in terms of classes and relationships between those classes.
A class icon is drawn as a 3-part box, with the class name in the top part, a list of attributes in the middle part, and a list of operations in the bottom part.
![](Images/01_Class.gif)
Attributes define the data to be held about a class. In object-oriented programming, they are mapped to fields, or data members. The format for attributes is:
visibility name: type = default_value
The visibility is as follows:
- Private + Public # Protected
You can use brief format (names only, or names and types without visibility). You can set an attribute to be treated as an array of attributes shown with square braces [ ] beside the name.
Operations describe what a class can do. They have access, a parameter list, return types and exceptions. The format for operations is:
visibility name (parameters): type
Parameters in the list are separated from each other by commas. Parameters take the format:
name: type = default_value
You can use brief format for operations (names with brackets).
UML 2 allows you to add a fourth part to the class icon if necessary. This part can contain icons of nested classes.
Associations show most general relationships between classes. An association provides a pathway for communication. If two objects are usually considered independently, the relationship is an association. Associations can be unidirectional and bi-directional. The relationship multiplicity indicates the number of links between each instance of the classes.
![](Images/01_Associations.gif)
In C++, an association occurs when one of the classes describes a pointer (reference) to an object of another class that was created somewhere independently. Having a pointer allows you to call member functions, or access data elements (if classes are friends):
class ClassB { . . . }; class ClassA { . . . ClassB* pB; . . . };
Unidirectional association presupposes mutual awareness. In order to allocate pointers, one of the classes must be pre-declared (without definition):
class ClassA; class ClassB { . . . ClassA* pA; . . . }; class ClassA { . . . ClassB* pB; . . . };
Aggregations are a specialized form of association used to model containment in whole-part relationships. an entity is contained in another entity or cannot be created and exist without an outer entity.
![](Images/01_Aggregation.gif)
In C++, aggregation is implemented by defining a pointer to a nested entity. The object is created as needed. It is good practice to check if an object has been created and delete it from memory in the destructor:
class ClassPart { . . . }; class ClassWhole { private: ClassPart* part = nullptr; . . . public: void someFunc() { part = new ClassPart(); . . . } . . . ~ClassWhole() { if (part != nullptr) { delete part; } } };
Sometimes a more strict form is used – a composition that assumes that the entity is completely contained in another entity. They are created and deleted together.
![](Images/01_Composition.gif)
In C++, the composition is implemented by defining a data member of type nested entity. The composition was considered in previous laboratory training.
Aggregation and composition allow you to specify the multiplicity.
A dependency is a relationship between two model elements in which a change to one model element will affect the other model element.
![](Images/01_Dependency.gif)
At the level of the programming language, this can be realized by calling static functions of another class or creating a temporary object. On C ++ it can look like this:
class Supplier { . . . public: static void f() { } void g() { } . . . }; class Client { . . . public: void h() { Supplier::f(); Supplier supplier; supplier.g(); . . . } . . . };
2.2 Inheritance
2.2.1 The Syntax of Inheritance
Inheritance is used to declare a new class, called a derived class, based on existing classes, known as base classes. Derived classes inherit, and can extend, the members defined in the base classes.
When you declare a class, you can indicate what class it derives from by writing a colon after the class name,
the type of derivation (public
or otherwise), and the class from which it derives:
class PublicTransport // Base class { . . . }; class Tram : public PublicTransport // Derived class { . . . };
In single inheritance, a common form of inheritance, classes have only one base class. The previous example shows syntax of single inheritance. Later versions of C++ introduced a multiple inheritance model for inheritance. In a multiple-inheritance graph, the derived classes may have a number of direct base classes:
class PublicTransport // Base class { . . . }; class RailTransport // Base class { . . . }; class Tram : public PublicTransport, public RailTransport // Derived class { . . . };
Member functions of derived class have only access to public and protected members of base classes, declared in
appropriate parts (public
and protected
). Unlike
public members, protected members of base classes can be used by member functions of derived classes. Private
members of base classes cannot be accessed from member functions of derived classes.
The C++ language supports the following types of inheritance:
- public base class: public members of the base class are public members of the derived class. Protected members of the base class are protected members of the derived class. Private members of the base class remain private to the base class
- protected base class: Both public and protected members of the base class are protected members of the derived class. Private members of the base class remain private to the base class
- private base class: Both public and protected members of the base class are private members of the derived class. Private members of the base class remain private to the base class.
Derived class inherits all members of base classes, except for constructors, destructors and overloaded assignment operation.
In the case where a class has one or more base classes, the base class constructors are invoked before the derived class constructor. The base class constructors are called in the order they are declared. Destructors are invoked in the reverse order.
Sometimes we need to invoke non-default constructors of base classes. Arguments for such constructors can be also passed using an initializer list:
class File { public: File(const char name) { ... } . . . }; class TextFile : public File { public: TextFile(const char name) : File(name) { ... } . . . };
Version C++ 11 adds a final
specifier, which in the context of class descriptions
indicates that a class cannot be used as a base class:
class CompletedEntity final { }; class DerivedFromFinal : public CompletedEntity // Syntax error! { };
The compiler will generate an error message "Error C3246 'DerivedFromFinal': cannot inherit from 'CompletedEntity' as it has been declared as 'final'".
To model inheritance in UML, generalization is used. A more general class is basic. The arrow starts from derived class.
![](Images/01_Inheritance.gif)
You can assign pointers to derived objects to pointers to objects of base types. Rules of reference initialization are the same:
class Base { public: someCommonFunc() { } . . . }; class FirstDerived : public Base { . . . }; class SecondDerived: public Base { . . . }; Base *pBase = new FirstDerived(); SecondDerived secondDerived; Base &refBase = secondDerived;
Other type casting operations for pointers and references requires use of
dynamic_cast
<type>(expression)
operator.
You can now create an array of pointers to the base class and initialize items with pointers to derived class objects. You can call some common function for all created objects:
Base *arr[3]; arr[0] = new Base(); arr[1] = new FirstDerived(); arr[2] = new SecondDerived(); for (int i = 0; i < 3; i++) { arr[i]->someCommonFunc(); }
In the definition of classes you can use the so-called using
-declaration. The using
-declaration
allows creation of a set of overloaded functions from base and derived classes; using
-declaration
cannot be used to get access to private data of a base class.
The declaration of a member function in the derived class does not overload, but hides member functions with
the same name declared in the base classes, even if their parameter lists are different. To make these functions
overloaded, you can use using
-declaration:
class Base { public: void setValue(int); }; class Descendant : private Base { public: using Base::setValue; void setValue(string); }; int main() { Descendant d; d.setValue("a"); d.setValue(2); // Without using-declaration would be a an error return 0; }
2.2.2 Features of Multiple Inheritance
Multiple inheritance is used in the case where the object represents a concept that combines several common concepts, each of which can be presented base class.
The ability to have more than one base class can cause repeated inclusion class as a base. This situation
occurs when the base classes have a common ancestor. Consider an example. The class Tram
is both
derived from PublicTransport
and RailTransport
, which in turn are derived from the
class Transport
. There is a so-called diamond inheritance:
class Transport { protected: char name[30]; double maxSpeed; }; class PublicTransport : public Transport { protected: int passengers; int maxLoad; }; class RailTransport : public Transport { protected: double trackGauge; double maxLoad; }; class Tram : public PublicTransport, public RailTransport { };
There are two problems when using such a hierarchy of classes:
- data members of the base class (
Transport
) are put separately intoPublicTransport
andRailTransport
classes, and then twice into theTram
class; - the
PublicTransport
andRailTransport
classes define data members with the same name but different types and different meanings; both data members are put intoTram
class and a name conflict occurs.
The so-called virtual inheritance is used to solve the first problem. You can add the virtual
keyword to the base class specification. One virtual base class object is shared between all classes that
specified it as virtual, defining their base classes:
class Transport { protected: char name[30]; double maxSpeed; }; class PublicTransport : virtual public Transport { protected: int passengers; int maxLoad; }; class RailTransport : virtual public Transport { protected: double trackGauge; double maxLoad; }; class Tram : public PublicTransport, public RailTransport { };
Constructors of virtual base classes are called before any constructors of non-virtual base classes.
Note: virtual base classes should not be confused with virtual functions, which will be discussed later; these concepts are unrelated.
Virtual inheritance in general does not solve the second problem: it does not help to avoid name conflicts. The
using
-declarations are used to solve problems with base class member names.
Multiple inheritance of classes regardless of the ways of implementation is dangerous in terms of possible name conflict, its use can lead to inconsistent and duplicate data. The use of multiple inheritance should be avoided in most cases. The only reason of its application assumes than one of the base class is abstract and does not contain data. Abstract classes are discussed below.
2.2.3 Hierarchies of Exception Classes
Unlike many other programming languages, C++ does not require that all exception classes would be derived from some common base class. But sometimes it is advisable to create own hierarchy of exceptions:
class Error { // ... }; class MathError : public Error { // ... };
Handler of base type also handles exceptions of derived types:
... catch (Error) { // handles Error and MathError exceptions }
Exceptions are caught in a bottom-down hierarchy: Specific (most derived classes) exceptions are handled first, followed by groups of exceptions (base classes), and, finally, a catch all handler. For example,
... catch (MathError) { // handles MathError type exceptions } catch (Error) { // handles Error type exceptions } catch (...) { // handles other exceptions }
2.3 Polymorphism
2.3.1 The Concept of Polymorphism
Polymorphism is a mechanism for definition of different functions with the same names based on the type of parameters (compile time polymorphism) or on the type of objects for which the methods are called (runtime polymorphism). The runtime polymorphism is built on the hierarchy of inheritance.
Sometimes it is necessary to create an array of pointers to different objects with different implementation of
specific actions for different items. One approach is based on the use of pointers to functions and creating
arrays of pointer type void
*
. It's not really safe way.
The C++ programming language provides alternative way. The runtime polymorphism in C++ uses inheritance hierarchies. This method is based on the above-described possibility of storing in pointers to the base type of addresses of objects of derived types. The exact object types in an array of pointers and addresses of member functions of these objects cannot be determined by the compiler. Polymorphism allows the processor to receive addresses of functions using pointers to objects.
2.3.2 Virtual Functions
A virtual function is a member function whose address can be determined dynamically at runtime. Virtual
functions are methods with a fixed calling interface, where the implementation may change in subsequent
derived classes. The virtual
keyword is used to notify compiler that declared method is a
virtual function:
class SomeClass { . . . public: virtual void firstFunc(); virtual int secondFunc(int x); };
We don't use virtual
keyword in definition of a function outside of class:
void SomeClass::firstFunc() { . . . // Implementation }
Derived classes can override all virtual methods or any part of them. Overridden functions must repeat
declarations of base functions using same resulting types and argument list. The virtual
keyword is not obligatory. Starting with version C++ 11, it is advisable to add the
override
modifier to the headers of overridden functions:
class Descendant : public SomeClass { . . . public: virtual void firstFunc() override { }; virtual int secondFunc(int x) override; };
An object of a class with virtual functions is also called polymorphic object of polymorphic class. Any polymorphic object contains a special invisible field for storing an address of its Virtual Method Table (VMT). Constructor creates the own VMT for each polymorphic class. VMT contains addresses of virtual functions of a given class. Those addresses can be used for call of virtual functions.
We can invoke any member function in three ways: using object variable, using pointer to an object and using reference to an object. In case of using object name compiler can obtain an address of a virtual function at the compile time. That is the simplest case. In other cases such address can be only obtained at runtime using VMT.
The practical application of virtual functions can be demonstrated by a classic example. When creating a
graphics system, you create a hierarchy of classes to represent geometric shapes. The base class
Shape
implements data members and member functions that can be used by various derived classes.
Such fields can, for example, include the current position (upper left corner of the rectangle in which the
figure is inscribed) – data members left
and top
, as well as sizes (data members
width
and height
). You can define methods for moving a shape and resizing it. The
virtual function draw()
will be overridden in derived classes. You should also add constructors,
access methods, and other functions:
class Shape { private: int left = 0, top = 0, width = 0, height = 0; public: void moveTo(int newLeft, int newTop) { left = newLeft; top = newTop; draw(); } void resize(int newWidth, int newHeight) { width = newWidth; height = newHeight; draw(); } virtual void draw() { } ... // other functions };
Specific classes derived from Shape
, such as Rectangle
or Ellipse
,
define the implementation of the draw()
method:
class Rectangle : public Shape { void draw() override { // drawing a rectangle } }; class Ellipse : public Shape { void draw() override { // drawing an ellipse } };
Now, for example, you can create an array of shapes and redraw the entire scene:
const int count = 4; Shape* shapes[count]; shapes[0] = new Ellipse(); shapes[1] = new Rectangle(); shapes[2] = new Ellipse(); shapes[3] = new Shape(); for (int i = 0; i < count; i++) { shapes[i]->draw(); }
In addition, the shapes can be arranged according to a certain rule:
for (int i = 0; i < count; i++) { shapes[i]->moveTo(i * 100, i * 200); }
Because the draw()
virtual function is called via the this
pointer
from the moveTo()
and resize()
functions, and therefore via the virtual method table,
the required shape will be drawn.
A destructor can be declared as virtual. This allows a pointer to a base class object to call the correct
destructor if this pointer actually refers to a derived class object. The destructor of a class derived from a
class with a virtual destructor is itself virtual. Virtual destructors supply information about size of
particular object. This information can be used by delete
operator by deallocation of
memory. It is a good idea to define virtual constructors for polymorphic classes.
Virtual functions cannot be static. Virtual functions can be inline, but this specifier has effect only for objects directly (not pointer or references to objects).
Sometimes in a certain derived class it is necessary to break the chain of redefining virtual functions. In
order to prevent further redefinition, the final
modifier is used after the
function header.
2.3.3 Abstract Classes
Sometimes, the base class should not provide the implementation of a virtual function because this class represents some basic abstraction. In this case, the function can be described as pure virtual function. A virtual function is specified as pure by setting it equal to zero:
virtual void f() = 0;
Pure virtual function is also called abstract function. An abstract class is a class with at least one pure virtual function:
class AbstractBaseClass { . . . public: virtual void f() = 0; virtual int secondFunc(int x); };
Derived classes must override pure virtual functions. Otherwise, such classes remain abstract.
For example, the Shape
class can be abstract because the default implementation of the draw()
function does not make sense:
class Shape { ... virtual void draw() = 0; ... };
You cannot create objects of abstract classes, but you can declare pointers and references to abstract classes.
Such pointers and references can be initialized with objects of derived classes. Therefore, in the previous
example, creating an object of type Shape
would result in a syntax error:
shapes[3] = new Shape(); // Syntax error
An error will also occur if in some derived class we did not define the draw()
function, or made a
mistake with its name and parameter list.
Fully abstract class (with no data and no non-abstract functions) is almost safe in terms of multiple inheritance. Such class can be used as one of the basic classes, simulating thus missing mechanism of interfaces and their implementation. A set of abstract functions defines a certain specific behavior that can be inherent to objects of different nature - the corresponding classes can belong to different hierarchies.
Consider
such an example. There is a base class Animal
and derived classes Insect
, Mammal
and
Bird
:
class Animal { //... }; class Insect : public Animal { //... }; class Mammal : public Animal { //... }; class Bird : public Animal { //... };
Some of the animals can fly. We create an abstract Flyable
class Flyable with an abstract
fly()
function and, accordingly, classes Bumblebee
, Bat
and Crow
,
in which the function fly()
is implemented in different ways:
class Flyable { public: virtual void fly() = 0; virtual ~Flyable() { } }; class Bumblebee : public Insect, public Flyable { public: virtual void fly() { std::cout << "Bumblebee flies\n"; } }; class Bat : public Mammal, public Flyable { public: virtual void fly() { std::cout << "Bat flies\n"; } }; class Crow : public Bird, public Flyable { public: virtual void fly() { std::cout << "Crow flies\n"; } };
Note: the destructor of Flyable
class is not abstract (it cannot be abstract), but its presence ensures
the virtuality of destructors in derived classes, which is very important for correct memory release.
Thanks to the possibilities of inheritance, pointers to objects of these types can be written into an array and
general actions can be performed (call of fly()
function):
int main() { Flyable* arr[] = { new Bumblebee(), new Bat(), new Crow() }; for (int i = 0; i < 3; i++) { arr[i]->fly(); } for (int i = 0; i < 3; i++) { delete arr[i]; } return 0; }
At the same time, a pointer to an object of type Bumblebee
can be put into an array of insects,
a pointer to an object of type Bat
can can be put into an array of mammals, a pointer to an object
of type Crow
can be put into an array of birds, and all of them can be allocated in an array of
pointers to animals.
2.4 Using Templates
2.4.1 Definition and Use of Template Functions
Generic programming is a programming paradigm that aims to separate scalar data, data structures, and algorithms for data processing. Template functions and type templates are key elements of generic programming in C++.
When the compiler encounters a template in code (it begins with the template
keyword)
it does not compile the template, but only finds where the template ends. In order to use the template, it is necessary
to specify its parameters. If a template is accessed with the specified formal parameters, the template code is
compiled.
Template functions intended for creation of generalized functions that can work with different types of data.
Definition and declaration of template function starts with template
keyword, followed with
a list of formal arguments enclosed in angle brackets ( "<
" and
">
") and separated by commas. List of formal arguments (parameters) of the template
cannot be empty. Each formal parameter, which defines the type, consists of word class
keyword, (or typename
keyword that is more consistent with modern standards)
followed by an identifier. Names of formal parameters must be unique.
Formal parameters of templates can be used to determine the result type and types of formal parameters. These parameters can be also used within a body of template function. In the following example, a template function will return sum of array items of arbitrary type. Important that array items should support assignment and "+=" operations, as well as assignment of constant "zero".
template <typename SomeType> SomeType sumOfArray(SomeType *a, const int size) { SomeType sum = 0; for (int i = 0; i < size; i++) { sum += a[i]; } return sum; }
Also, you can define a template function of output the array items to standard output stream:
template <typename SomeType> void printArray(SomeType *a, const int size) { for (int i = 0; i < size; i++) { cout << a[i] << ' '; } cout << endl; }
The compiler generates specific definitions of functions that match the template by invocation of these functions with parameters of specific types. The usage of previously defined template functions can be as follows:
int main() { int a[] = {1, 2, 3}; printArray(a, 3); cout << sumOfArray(a, 3) << endl; double b[] = {1.1, 2.2, 3.3, 4.5}; printArray(b, 4); cout << sumOfArray(b, 4) << endl; cin.get(); return 0; }
It is possible to define several function templates with the same name (that is, to overload it)
2.4.2 The Order of Calling Template Functions
In some cases some specific types need to give a special definition of template function. In these cases, the
programmer should specify a special version of the function. For example, a template function min()
can be applied to types for which the specified "<" operation:
template <typename Type> Type min(Type a, Type b) { return a < b ? a : b; }
This template is not appropriate for the string comparison. We can define a special implementation of a
function for char
*
type:
char* min(char* s1, char* s2) { return strcmp(s1, s2) < 0 ? s1 : s2; }
The order of functions invocation will be as follows:
- All variants of non-template functions are investigated.
- All variants of template functions are investigated.
- All variants of non-template functions are re-investigated with applying of type conversion.
In order to be able to specify a template compiler must see not only declaration but also definition of the function. Therefore, the definition of template functions can and should be placed in header files.
When you call the function actual template parameters can be specified explicitly, for example:
int i = min<int>(2, 3);
2.4.3 Class Templates
Class templates can be used for the creation of a family of classes that differs in types or constant values within the class body.
The previous declaration and definition of the template class begins with a keyword
template
followed by a list of formal parameters. This list cannot be empty. For
example:
template <typename T> class X { T t; public: void set(T t1) { t = t1; } };
Class template is not a class. Template instantiation is the creation of certain types from the template. Such classes are called template instances:
X<int> xi; X<double> xd;
Note: class templates sometimes conventionally called generic classes.
Template parameters can be types, parameters of conventional types and other templates. The template can have several parameters. Integer arguments are often used to set the size and boundaries of arrays. A template argument must be a constant. Default values of parameters can be set:
template <typename T, int size = 64> class Y { . . . };
The value supplied for a non-type template argument must be a constant expression:
const int N = 128; int i = 256; Y<int, 2*N> b1;// OK Y<float, i> b2;// Error: i is not constant
Class templates support mechanism of inheritance. You can create a template class derived from a template and from a "normal" class.
The template class member function is considered implicit template function with class template parameters.
Sometimes, the generic implementation of a member function can be not suitable for some types. In such cases,
you can explicitly implement this member for a specific type. The implementation should start with special
declaration template
<>
without parameters. .
#include <iostream> template <typename T> class MinFilder { private: T a, b; public: MinFilder(T a, T b) { this->a = a; this->b = b; } T findMin(); }; template <typename T> T MinFilder<T>::findMin() { return a < b ? a : b; } template <> const char* MinFilder<const char*>::findMin() { return std::strcmp(a, b) < 0 ? a : b; } int main() { MinFilder<const char*> finder("a", "b"); std::cout << finder.findMin(); }
You could also give special class definition of a class template, designed for a particular type. Friend functions of template classes are not implicit template functions.
Template class can have static data members. Each class generated from the template has its own copy static data members.
2.4.4 Class Templates and Compliance of Types
Types that were created from some template are different and do not allow implicit conversion (except for identical lists of actual parameters). For example:
template <typename T> class X { /* ... */ } X<int> x1; X<short> x2; X<int> x3;
Now x1
and x3
are of the same type, but x2
is not. Automatic type
casting is not done.
x2 = x3; // error
3 Sample Programs
3.1 Hierarchy of Real World Objects
Suppose you want to develop the following class hierarchy: "Region" - "Populated region" - "Country". Certain classes of this hierarchy can be used as base classes for other classes (for instance, "Uninhabited island", "National park", "Borough", "Autonomous Republic", etc.). Class hierarchy can be expanded by classes "City" and "Island." It is advisable to add constructors that initialize all fields. We can also create an array of pointers to different objects of the hierarchy and display data that represents objects of different types.
We can offer such a class hierarchy, in which for greater clarity all functions are implemented within the classes::
#pragma warning(disable:4996) #include <cstring> #include <iostream> using std::strcpy; using std::cout; using std::endl; class Region { private: char name[30]; double area; public: Region(const char *name, double area) { strcpy(this->name, name); this->area = area; } char* getName() { return name; } double getArea() const { return area; } virtual void show() { cout << endl << "Name: " << name << ".\tArea: " << area << " sq.km."; } virtual ~Region() { } }; class PopulatedRegion : public Region { private: int population; public: PopulatedRegion(const char* name, double area, int population) : Region(name, area) { this->population = population; } int getPopulation() const { return population; } double density() const { return population / getArea(); } void show() override { Region::show(); cout << "\tPopulation:" << population << " inh.\tDensity: " << density() << " inh./sq.km."; } }; class Country : public PopulatedRegion { private: char capital[20]; public: Country(const char* name, double area, int population, const char* capital) : PopulatedRegion(name, area, population) { strcpy(this->capital, capital); } public: char* getCapital() { return capital; } void show() override { PopulatedRegion::show(); cout << "\tCapital" << capital; } }; class City : public PopulatedRegion { private: int boroughs; public: City(const char* name, double area, int population, int boroughs) : PopulatedRegion(name, area, population) { this->boroughs = boroughs; } public: int getBoroughs() const { return boroughs; } void show() override { PopulatedRegion::show(); cout << "\tBoroughs: " << boroughs; } }; class Island : public PopulatedRegion { private: char sea[30]; public: Island(const char* name, double area, int population, const char* sea) : PopulatedRegion(name, area, population) { strcpy(this->sea, sea); } char* getSea() { return sea; } virtual void show() override { PopulatedRegion::show(); cout << "\tSea: " << sea; } }; void main() { const int N = 4; Region *regions[N] = { new City("Kyiv", 839, 2679000, 10), new Country("Ukraine", 603700, 46294000, "Kyiv"), new City("Kharkiv", 310, 1461000, 9), new Island("Zmiyinyy", 0.2, 30, "Black Sea") }; for (int i = 0; i < N; i++) { regions[i]->show(); } for (int i = 0; i < N; i++) { delete regions[i]; } }
Note: the inclusion of nonstandard directive #pragma warning(disable:4996)
in Visual
Studio allows to avoid error messages concerned with the use of "dangerous" strcpy()
function.
We use an array of pointers instead of array objects, because only in this case the mechanism of polymorphism
can work. Otherwise the show()
function would be invoked only from the base class.
3.2 Class for Solving Equation using Dichotomy Method
An example of solving the problem of finding the root of the equation by dividing in half the segment that has been shown in previous laboratory work opportunities can be modified using polymorphism.
After a new empty project was created, we can add a new class, AbstractDichotomy
. This can be done
by using the main menu function Project | Add Class.... Enter the class name
AbstractDichotomy
and click OK. The pair of files is automatically generated: header file
and implementation file. The AbstractDichotomy.h
file contains the following code:
#pragma once class AbstractDichotomy { };
The AbstractDichotomy.cpp
file contains header file inclusion:
#include "AbstractDichotomy.h"
Generated text contains a non-standard preprocessor directive #pragma once
. The use of this
directive instead of the standard inclusion guards make it impossible to compile this code in other environments
than Visual Studio. We'll replace this directive with inclusion guards. Such replacement should be performed for
each generated class. We'll declare the function of solving equation root()
and pure virtual
function f()
:
#ifndef AbstractDichotomy_h #define AbstractDichotomy_h class AbstractDichotomy { public: double root(double a, double b, double eps = 0.001); virtual double f(double x) = 0; }; #endif
The implementation file contains the definition of the root()
function:
#include <cmath> #include "AbstractDichotomy.h" double AbstractDichotomy::root(double a, double b, double eps) { double x; do { x = (a + b) / 2; if (f(a) * f(x) > 0) { a = x; } else { b = x; } } while (std::fabs(b - a) > eps); return x; }
Now the program that needs solving equations should contain the inclusion of our header file and the definition of a new derived class. This class will define a function that represents a first member of equation:
#include <iostream> #include "AbstractDichotomy.h" using std::cout; using std::endl; class MyDichotomy : public AbstractDichotomy { virtual double f(double x) override { return x * x - 2; } }; void main() { MyDichotomy d; cout << d.root(0, 6) << endl; // 1.41431 cout << d.root(0, 6, 0.00001) << endl; // 1.41421 }
New classes should be created for each function.
3.3 Using Templates to Create an Universal Function
An alternative version of a universal solution for creating a universal root finding function is provided by templates.
We create a header file:
#ifndef DICHOTOMY_H #define DICHOTOMY_H template <typename F> double root(F f, double a, double b, double eps = 0.001) { double x; do { x = (a + b) / 2; if (f(a) * f(x) > 0) { a = x; } else { b = x; } } while (b - a > eps); return x; } #endif
A separate implementation file is not required.
In the file with the main()
function, we allocate the class for creating a functional object. In addition,
we can create a function for which the root will also be found. We find roots of both functions
in the main()
function..
#include <iostream> #include <cmath> #include "Dichotomy.h" // A class for creating a functional object: a polynomial of the Nth degree. // The template parameter is an integer constant that can be applied to create an array. template <int N> class Polynomial { private: double coefs[N + 1] = { 0 }; public: Polynomial(std::initializer_list<double> coefs) { int i = 0; for (const double& k : coefs) { if (i <= N) { this->coefs[i++] = k; } } } // Thanks to the parentheses operation overload // we can work with the object as with the function double operator()(double x) { double sum = 0; double p = 1; for (int i = N; i >= 0; i--) { sum += p * coefs[i]; p *= x; } return sum; } }; double my_sin(double x) { return std::sin(x); } int main() { std::cout << root(my_sin, 1, 4, 0.000001) << std::endl; Polynomial<2> poly = { 1, -1, -2 }; std::cout << root(poly, 0, 3, 0.000001) << std::endl; for (double x = 0; x < 3; x += 0.25) { std::cout << x << "\t" << poly(x) << std::endl; } return 0; }
Unfortunately, unlike using pointers to functions, a template-based solution does not allow you to apply standard functions. As in the above example, they must be "wrapped" with our functions.
3.4 Class Template for Presentation of One-Dimensional Array
The class that was created in the previous laboratory work can be converted into a class template so that we
can create an array of different types. The class can be placed in a separate header file called
Array.h
. A template function called getSum()
, which is located in the same file,
allows us to find the sum of array items. The code of Array.h
file will be as follows:
#ifndef Array_h #define Array_h #include <iostream> using std::istream; using std::ostream; template <typename T> class Array { friend ostream& operator<<(ostream& out, const Array& a) { for (int i = 0; i < a.size; i++) { out << a.pa[i] << ' '; } return out; } friend istream& operator >> (istream& in, Array& a) { for (int i = 0; i < a.size; i++) { in >> a.pa[i]; } return in; } private: T *pa = nullptr; int size = 0; public: class OutOfBounds { int index; public: OutOfBounds(int i) : index(i) { } int getIndex() const { return index; } }; Array() { } Array(int n); Array(Array& arr); ~Array() { if (pa) { delete[] pa; } } void addElem(T elem); T& operator[](int index); const Array& operator=(const Array& a); bool operator==(const Array& a) const; int getSize() const { return size; } }; template <typename T> Array<T>::Array(int n) { pa = new T[size = n]; } template <typename T> Array<T>::Array(Array& arr) { size = arr.size; pa = new T[size]; for (int i = 0; i < size; i++) { pa[i] = arr.pa[i]; } } template <typename T> void Array<T>::addElem(T elem) { T *temp = new T[size + 1]; if (pa) { for (int i = 0; i < size; i++) { temp[i] = pa[i]; } delete[] pa; } pa = temp; pa[size] = elem; size++; } template <typename T> T& Array<T>::operator[](int index) { if (index < 0 || index >= size) { throw OutOfBounds(index); } return pa[index]; } template <typename T> const Array<T>& Array<T>::operator=(const Array<T>& a) { if (&a != this) { if (pa) { delete[] pa; } size = a.size; pa = new T[size]; for (int i = 0; i < size; i++) { pa[i] = a.pa[i]; } } return *this; } template <typename T> bool Array<T>::operator==(const Array<T>& a) const { if (&a == this) { return true; } if (size != a.size) { return false; } for (int i = 0; i < size; i++) { if (pa[i] != a.pa[i]) { return false; } } return true; } template <typename T> T getSum(Array<T>& a) { T sum = T(); for (int i = 0; i < a.getSize(); i++) { sum = sum + a[i]; } return sum; } #endif
To perform testing of our class for different types, we can create Vector.h
header file and copy
source code of the class Vector
previous laboratory into this header file:
#ifndef Vector_h #define Vector_h #include <iostream> using std::istream; using std::ostream; class Vector { friend istream& operator >> (istream& in, Vector& v) { return in >> v.x >> v.y; } friend ostream& operator<<(ostream& out, const Vector& v) { return out << "x=" << v.x << " y=" << v.y; } friend Vector operator+(Vector v1, Vector v2) { return Vector(v1.x + v2.x, v1.y + v2.y); } friend Vector operator*(double k, Vector v) { return Vector(v.x * k, v.y * k); } friend Vector operator*(Vector v, double k) { return operator*(k, v); } friend double operator*(Vector v1, Vector v2) { return v1.x * v2.x + v1.y * v2.y; } private: double x, y; public: Vector() { x = y = 0; } Vector(double x, double y) { this->x = x; this->y = y; } double getX() { return x; } void setX(double x) { this->x = x; } double getY() { return y; } void setY(double y) { this->y = y; } }; #endif
Now you can use the template array class to store as integers and vectors:
#include <iostream> #include "Array.h" #include "Vector.h" using std::cout; using std::endl; void main() { Array<int> intArray; intArray.addElem(11); intArray.addElem(12); cout << intArray << endl; try { intArray[1] = 4; intArray[10] = 35; } catch (Array<int>::OutOfBounds e) { cout << "Bad index: " << e.getIndex() << endl; } cout << getSum(intArray) << endl; Array<Vector> vectorArray; vectorArray.addElem(Vector(1, 2)); vectorArray.addElem(Vector(3, 4)); vectorArray.addElem(Vector(5, 6)); cout << vectorArray << endl; cout << getSum(vectorArray) << endl; // x=9 y=12 }
You can also store items of other data types, such as pointers to integer:
int m = 1, n = 2; // Create an array of pointers to int Array<int*> pointerArray; pointerArray.addElem(&m); pointerArray.addElem(&n); // Output dereferenced values: cout << *pointerArray[0] << " " << *pointerArray[1] << endl; // 1 2
But trying to find the sum of items will result in compilation error because you cannot find the sum of pointers:
// You cannot find the sum of pointers: cout << getSum(pointerArray) << endl; // Compile error (cannot add two pointers)
4 Exercises
- Create a hierarchy of classes "Building" - "Educational building". Create an array of pointers and display in a cycle data that represents objects of different types.
- Create a hierarchy of classes "Digital device" - "Mobile phone". Create an array of pointers and display in a cycle data that represents objects of different types.
- Create a template function for searching the array items that are in a certain range. Test function for different types of items.
- Create a template class for storing pairs of different types.
5 Quiz
- How to use UML to represent a class?
- What connections between classes does UML offer and how are they implemented in C++?
- What is the concept of inheritance?
- What are differences between public, protected, and private inheritance?
- What are the advantages and disadvantages of multiple inheritance?
- What is the usage of virtual base classes?
- What is the usage of a hierarchy of exceptions?
- What is the concept of polymorphism?
- What is the difference between compile time polymorphism and runtime polymorphism?
- What is a virtual function?
- What classes are considered to be polymorphic?
- Why polymorphic classes require definition of virtual destructors?
- What is a virtual method table?
- What is pure virtual function?
- What is an abstract class?
- What is the use of template functions?
- Is it possible to implement template function separately for certain type?
- When you need to specify explicitly a template parameter of template functions?
- How to create a class template?
- What are the template parameters?
- What is the use of integer template parameters?
- What is template instantiation?
- What are rules of type conversion for objects of instantiated types?