AhmadLang / Object Oriented Programing using C++
.pdf
Object Oriented Programming using C++
In this example the value std::bad_cast is introduced. The std::bad_cast is thrown as an exception if the dynamic cast of a reference to a base class object fails. Apparently bad_cast is the name of a type ( ). In section EMPTYENUM the construction of such a type is discussed.
The dynamic cast operator is a handy tool when an existing base class cannot or should not be modified (e.g., when the sources are not available), and a derived class may be modified instead. Code receiving a base class pointer or reference may then perform a dynamic cast to the derived class to be able to use the derived class' functionality.
Casts from a base class reference or pointer to a derived class reference or pointer are called downcasts.
The typeid operator
As with the dynamic_cast<>() operator, the typeid is usually applied to base class objects, that are actually derived class objects. Similarly, the base class should contain one or more virtual functions.
In order to use the typeid operator, source files must
#include <typeinfo>
Actually, the typeid operator returns an object of type type_info, which may, e.g., be compared to other type_info objects.
The class type_info may be implemented differently by different implementations, but at the very least it has the following interface:
class type_info
{
public:
virtual ~type_info();
int operator==(const type_info &other) const; int operator!=(const type_info &other) const; char const *name() const;
private:
type_info(type_info const &other);
type_info &operator=(type_info const &other);
};
Note that this class has a private copy constructor and overloaded assignment operator. This prevents the normal construction or assignment of a type_info object. Type_info objects are constructed and returned by the typeid operator. Implementations, however, may choose to extend or elaborate the type_info class and provide, e.g., lists of functions that can be called with a certain class.
If the type_id operator is given a base class reference (where the base class contains at least one virtual function), it will indicate that the type of its operand is the derived class. For example:
class Base; // contains >= 1 virtual functions class Derived: public Base;
Derived
d;
Base
111
Object Oriented Programming using C++
&br = d;
cout << typeid(br).name() << endl;
In this example the typeid operator is given a base class reference. It will print the text ``Derived'', being the class name of the class br actually refers to. If Base does not contain virtual functions, the text ``Base'' would have been printed.
The typeid operator can be used to determine the name of the actual type of expressions, not just of class type objects. For example:
cout << typeid(12).name() << endl; |
// prints: |
int |
cout << typeid(12.23).name() << endl; |
// prints: |
double |
Note, however, that the above example is suggestive at most of the type that is printed. It may be int and double, but this is not necessarily the case. If portability is required, make sure no tests against static, built-in strings are required. Check out what your compiler produces in case of doubt.
In situations where the typeid operator is applied to determine the type of a derived class, it is important to realize that a base class reference is used as the argument of the typeid operator. Consider the following example:
class Base; // contains at least one virtual function class Derived: public Base;
Base |
|
|
*bp = new Derived; |
// base class pointer to derived object |
|
if (typeid(bp) == typeid(Derived *)) |
// 1: false |
|
... |
|
|
if (typeid(bp) == typeid(Base *)) |
// 2: true |
|
... |
|
|
if (typeid(bp) == typeid(Derived)) |
// 3: false |
|
... |
|
|
if (typeid(bp) == typeid(Base)) |
// 4: false |
|
... |
|
|
Here, (1) returns false as a Base * is not a Derived *. (2) returns true, as the two pointer types are the same, (3) and (4) return false as pointers to objects are not the objects themselves.
On the other hand, if *bp is used in the above expressions, then (1) and (2) return false as an object (or reference to an object) is not a pointer to an object, whereas with
if (typeid(*bp) == typeid(Derived)) |
// 3: true |
... |
|
if (typeid(*bp) == typeid(Base)) |
// 4: false |
... |
|
we see that (3) now returns true: *bp actually refers to a Derived class object, and typeid(*bp) will return typeid(Derived).
A similar result is obtained if a base class reference is used:
Base
&br = *bp;
112
Object Oriented Programming using C++
if (typeid(br) == typeid(Derived)) |
// 3: true |
... |
|
if (typeid(br) == typeid(Base)) |
// 4: false |
... |
|
A polymorphic exception class
Earlier we hinted at the possibility of designing a class Exception whose process() member would behave differently, depending on the kind of exception that was thrown. Now that we've introduced polymorphism, we can further develop this example.
It will now probably be clear that our class Exception should be a virtual base class, from which special exception handling classes can be derived. It could even be argued that Exception can be an abstract base class declaring only pure virtual member functions. The (abstract) base class Exception is designed as follows:
#ifndef _EXCEPTION_H_ #define _EXCEPTION_H_
#include <iostream> #include <string>
class Exception
{
friend ostream &operator<<(ostream &str, Exception const &e)
{
return str << e.operator string();
}
public:
virtual ~Exception() {}
virtual void process() const = 0; virtual operator string() const
{
return reason;
}
protected:
Exception(char const *reason)
:
reason(reason)
{} string
reason;
};
#endif
The operator string() member function of course replaces the toString() member. The friend operator<<() function is using the (virtual) operator string() member so that we're able to insert an Exception object into an ostream. Apart from that, notice the use of a virtual destructor, doing nothing.
A derived class FatalException: public Exception could now be defined as follows (using a very basic process() implementation indeed):
#ifndef _FATALEXCEPTION_H_ #define _FATALEXCEPTION_H_
#include "exception.h"
class FatalException: public Exception
{
public:
FatalException(char const *reason)
113
Object Oriented Programming using C++
:
Exception(reason)
{}
void process() const
{
exit(1);
}
};
#endif
(using derived classes WarningException and MessageException), constructed like FatalException:
#include <iostream>
#include "message.h" #include "warning.h"
void initialExceptionHandler(Exception const *e)
{
cout << *e << endl; // show the plain-text information
if
(
dynamic_cast<MessageException const *>(e)
||
dynamic_cast<WarningException const *>(e)
) |
|
{ |
|
e->process(); |
// Process a message or a warning |
delete e; |
|
} |
|
else |
|
throw; |
// Pass on other types of Exceptions |
}
How polymorphism is implemented
This section briefly describes how polymorphism is implemented in C++. It is not necessary to understand how polymorphism is implemented if using this feature is the only intention. However, we think it's nice to know how polymorphism is at all possible. Besides, the following discussion does explain why there is a cost of polymorphism in terms of memory usage.
The fundamental idea behind polymorphism is that the compiler does not know which function to call compile-time; the appropriate function will be selected run-time. That means that the address of the function must be stored somewhere, to be looked up prior to the actual call. This `somewhere' place must be accessible from the object in question. E.g., when a Vehicle *vp points to a Truck object, then vp->getweight() calls a member function of Truck; the address of this function is determined from the actual object which vp points to.
A common implementation is the following: An object containing virtual member functions holds as its first data member a hidden field, pointing to an array of pointers containing the addresses of the virtual member functions. The hidden data member is usually called the vpointer, the array of virtual member function addresses the vtable. Note that the discussed implementation is compiler-dependent, and is by no means dictated by the C++ ANSI/ISO standard.
The table of addresses of virtual functions is shared by all objects of the class. Multiple classes may even share the same table. The overhead in terms of memory consumption is therefore:
One extra pointer field per object, which points to:
114
Object Oriented Programming using C++
One table of pointers per (derived) class to address the virtual functions. Consequently, a statement like vp->getweight() first inspects the hidden data member of the object pointed to by vp. In the case of the vehicle classification system, this data member points to a table of two addresses: one pointer for the function getweight() and one pointer for the function setweight(). The actual function which is called is determined from this table.
The internal organization of the objects having virtual functions is further illustrated in figure 15.
figure 15: Internal organization objects when virtual functions are defined.
As can be seen from figure 15, all objects which use virtual functions must have one (hidden) data member to address a table of function pointers. The objects of the classes Vehicle and Auto both address the same table. The class Truck, however, introduces its own version of getweight(): therefore, this class needs its own table of function pointers.
Templates
Function templates
Templates allow to create generic functions that admit any data type as parameters and return value without having to overload the function with all the possible data types. Until certain point they fulfill the functionality of a macro. Its prototype is any of the two following ones:
template <class indetifier> function_declaration; template <typename indetifier> function_declaration;
the only difference between both prototypes is the use of keyword class or typename, its use is indistinct since both expressions have exactly the same meaning and behave exactly the same way.
For example, to create a template function that returns the greater one of two objects we could use:
template <class GenericType>
GenericType GetMax (GenericType a, GenericType b) { return (a>b?a:b);
}
115
Object Oriented Programming using C++
As specifies the first line, we have created a template for a generic data type that we have called GenericType. Therefore in the function that follows, GenericType becomes a valid data type and it is used as type for its two parameters a and b and as return value for the function GetMax.
GenericType still does not represent any concrete data type; when the function GetMax will be called we will be able to call it with any valid data type. This data type will serve as pattern and will replace GenericType in the function. The way to call a template class with a type pattern is the following:
function <pattern> (parameters);
Thus, for example, to call GetMax and to compare two integer values of type int we can write:
int x,y;
GetMax <int> (x,y);
so GetMax will be called as if each appearance of GenericType was replaced by an int expression.
Ok, here is the complete example:
// function template |
6 |
#include <iostream.h> |
10 |
template <class T> T GetMax (T a, T b) {
T result;
result = (a>b)? a : b; return (result);
}
int main () { int i=5, j=6, k;
long l=10, m=5, n; k=GetMax<int>(i,j); n=GetMax<long>(l,m); cout << k << endl; cout << n << endl; return 0;
}
(In this case we have called the generic type T instead of GenericType because it is shorter and in addition is one of the most usual identifiers used for templates, although it is valid to use any valid identifier).
In the example above we used the same function GetMax() with arguments of type int and long having written a single implementation of the function. That is to say, we have written a function template and called it with two different patterns.
As you can see, within our GetMax() template function the type T can be used to declare new objects:
T result;
result is an object of type T, like a and b, that is to say, of the type that we enclose between anglebrackets <> when calling our template function.
116
Object Oriented Programming using C++
In this concrete case where the generic T type is used as parameter for function GetMax the compiler can find out automatically which data type is passed to it without having you to specify it with patterns <int> or <long>. So we could have written:
int i,j; GetMax (i,j);
since being both i and j of type int the compiler would assume automatically that the wished function is for type int. This implicit method is more usual and would produce the same result:
// function template II |
6 |
#include <iostream.h> |
10 |
template <class T> T GetMax (T a, T b) {
return (a>b?a:b);
}
int main () { int i=5, j=6, k;
long l=10, m=5, n; k=GetMax(i,j); n=GetMax(l,m); cout << z << endl; cout << n << endl; return 0;
}
Notice how in this case, within function main() we called our template function GetMax() without explicitly specifying the type between angle-brackets <>. The compiler automatically determines what type is needed on each call.
Because our template function includes only one data type (class T) and both arguments it admits are both of that same type, we cannot call to our template function with two objects of different types as parameters:
int i; long l;
k = GetMax (i,l);
It would be incorrect, since our function waits for two arguments of the same type (or class).
We can also make template-functions that admit more than one generic class or data type. For example:
template <class T, class U> T GetMin (T a, U b) {
return (a<b?a:b);
}
In this case, our template function GetMin() admits two parameters of different types and returns an object of the same type as the first parameter (T) that is passed. For example, after that declaration we could call the function writing:
int i,j; long l;
i = GetMin<int,long> (j,l);
or even
i = GetMin (j,l);
even although j and l are of different types.
117
Object Oriented Programming using C++
Class templates
We also have the possibility to write class templates, so that a class can have members based on generic types that do not need to be defined at the moment of creating the class or whose members use these generic types. For example:
template <class T> class pair {
T values [2]; public:
pair (T first, T second)
{
values[0]=first; values[1]=second;
}
};
The class that we have just defined serves to store two elements of any valid type. For example, if we wanted to declare an object of this class to store two integer values of type int with the values 115 and 36 we would write:
pair<int> myobject (115, 36);
this same class would serve also to create an object to store any other type:
pair<float> myfloats (3.0, 2.18);
The only member function has been defined inline within the class declaration, nevertheless if this is not thus and we define a function member outside the declaration we always must also precede the definition with the prefix template <... >.
// class templates |
100 |
#include <iostream.h>
template <class T> class pair {
T value1, value2; public:
pair (T first, T second) {value1=first; value2=second;}
T getmax ();
};
template <class T> T pair<T>::getmax ()
{
T retval;
retval = value1>value2? value1 : value2; return retval;
}
int main () {
pair <int> myobject (100, 75); cout << myobject.getmax(); return 0;
}
notice how the definition of member function getmax begins:
template <class T> T pair<T>::getmax ()
118
Object Oriented Programming using C++
All Ts that appear are necessary, reason why whenever you declare member functions you will have to follow a format similar to this (the second T makes reference to the type returned by the function, so this may vary).
Template specialization
A template specialization allows a template to make specific implementations for when the pattern is of a determined type. For example, suppose that our class template pair included a function to return the result of the module operation between the objects contained in it, but we only want that it works when the contained type is int and for the rest of types we want that this function always returns 0. This can be done this way:
// Template specialization |
25 |
#include <iostream.h> |
0 |
template <class T> class pair {
T value1, value2; public:
pair (T first, T second) {value1=first; value2=second;}
T module () {return 0;}
};
template <> class pair <int> {
int value1, value2; public:
pair (int first, int second) {value1=first; value2=second;}
int module ();
};
template <>
int pair<int>::module() { return value1%value2;
}
int main () {
pair <int> myints (100,75);
pair <float> myfloats (100.0,75.0); cout << myints.module() << '\n'; cout << myfloats.module() << '\n'; return 0;
}
As you can see in the code the specialization is defined this way:
template <> class class_name <type>
The specialization is part of a template, for that reason we must begin the declaration with template <>. And indeed because it is a specialization for a concrete type the generic type cannot be used in it, as well as the first angle-brackets <> must appear empty. After the class name we must include the type that is being specialized enclosed between angle-brackets <>.
When we specialize a type of a template we must also define all the members adequating them to the specialization (if one pays attention, in the example above we have had to include its own constructor, although it is identic to the one in the generic template). The reason is that no member is "inherited" from the generic template to the specialized one.
119
Object Oriented Programming using C++
Parameter values for templates
Besides the template arguments preceded by class or typename keyword that represent a type, functions templates and class templates can include other parameters that are not types whenever they are also constant values, like for example values of fundamental types. As an example see this class template that serves to store arrays:
// array template |
100 |
#include <iostream.h> |
3.1416 |
template <class T, int N> class array {
T memblock [N]; public:
setmember (int x, T value); T getmember (int x);
};
template <class T, int N> array<T,N>::setmember (int x, T value) {
memblock[x]=value;
}
template <class T, int N>
T array<T,N>::getmember (int x) { return memblock[x];
}
int main () {
array <int,5> myints; array <float,5> myfloats;
myints.setmember (0,100); myfloats.setmember (3.0,3.1416); cout << myints.getmember(0) << '\n'; cout << myfloats.getmember(3) << '\n'; return 0;
}
It is also possible to set default values for any template parameter just as that is done with function parameters.
Some possible template examples seen above:
template <class T> |
// The most usual: one class parameter. |
template <class T, class U> |
// Two class parameters. |
template <class T, int N> |
// A class and an integer. |
template <class T = char> |
// With a default value. |
template <int Tfunc (int)> |
// A function as parameter. |
Templates and multiple-file projects
From the point of view of the compiler, templates are not normal function or classes. They are compiled on demand. Meaning that the code of a template function is not compiled until an instantiation is required. At that moment, when an instantiation is required, the compiler generates from the template a function specifically for that type.
When projects grow it is usual to split the code of a program in different source files. In these cases, generally the interface and implementation are separated. Taking as example a library of functions, the interface generally consists on the prototypes of all the functions that can be called,
120
