|
|
Summary
63) Template Functions
64) Overloading Template Functions
65) Explicitly Specifying the Type in a Template Function
66) Template Functions and Objects
67) Recap
In the previous lecture, we talked about the objects used as data members within
classes.
In this case, you will find that there is a lot of code reuse. A completely debug
code is
used as building blocks for developing new and more complex classes. Today’s
discussion will mainly focus on a new way of code reuse with an entirely different
style
of reuse. This method is called templates.
Template Functions
There are two different types of templates in C++ language i.e.’ function templates
and
class templates. Before going ahead, it will be sagacious to know what a template
is? You
have used a lot of templates in the childhood. There are small scales being marketed
at
the stationary shops having some figures on them like circle, a square, rectangle
or a
triangle. We have been using these articles to draw these shapes on the paper. We
put the
scale on the paper and draw the lines with the pencil over that figure to get that
shape.
These engraved shapes are generally called stencils. But in a way, these are also
templates. We may also take these ‘cut-outs’ as sketches. So a template is a sketch
to
draw some shape or figure. While drawing a special design, say of furniture, we
develop
a template for this, which is not an actual piece of furniture. We try that its
shape should
be like the outline. Later, the cut out prepared out of wood in line with the template,
is
actual piece of furniture. We can think of making a triangular template and then
drawing
it on a piece of wood and shaping it into a triangle. We can use the same template
and put
it on a piece of metal and can cut it into a triangle and so on. In a way, that
template is
Page 528
allowing us the reuse of a certain shape. This is the concept we are going to try
and build
on here.
Here we are going to discuss the benefits of the function templates. We have been
using a
swap function. We want to interchange two things. You know the technique
that we need
a third temp-place holder. If we want to swap two integers
i and
j, the code will be as
under:
void swap(int &i, int &j)
{
int tmp;
tmp = i;
i = j;
j = tmp;
}
This is a very generic way of interchanging two values. We have written a swap function
to interchange two integers. To interchange two doubles, we have to come up with
some
other swap function for doubles and so on. Whenever, a need to use this swapping
technique for different data type arises, we have to write a new function. Can we
write
such functions? Yes, we can. These functions can be overloaded. We can have functions
with the same name as long as the types or the number or the arguments are different.
Compiler can detect which function should be used. It will call that function
appropriately. So you can define
swap for integers, floats
and doubles. There is also no
problem in defining multiple versions of this function with different data types.
Depending on what is required, the compiler will automatically make a call to the
correct
function. This is the overloading. The code for every data type looks like:
void swap(SomeDataType &firstThing, SomeDataType &secondThing)
{
SomeDataType tmp;
tmp = firstThing;
firstThing = secondThing;
secondThing = tmp;
}
This is a sort of generic code, we are writing again and again for different data
types. It
will be very nice if somehow we can write the code once and let the compiler or
language
handle everything else. This way of writing is called templates or function templates.
As
seen in the example of a template of a triangle, we will define a generic function.
Once it
is defined and determined where it will be called for some specific data type, the
compiler will automatically call that function.
As discussed in the example of overloaded functions, the automatic part is also
there.
But we wrote all those functions separately. Here the automatic part is even deeper.
In
other words, we write one template function without specifying a data type. If it
is to be
Page 529
called for int data
type, the compiler will itself write an
int version of that function.
If it is
to be called for double, the compiler will itself write it. This does not happen
at run time,
but at compile time. The compiler will analyze the program and see for which data
type,
the template function has been called. According to this, it will get the template
and write
a function for that data type.
Now, we will see the idea or technique for defining template function. Here you
will
come across some new keywords. First keyword is “template”. These are the recent
addition to the language. Some old compilers may not have these features. However,
now
almost all of the compilers implement these features. Another keyword is
class that is
quite different than we have been using for defining the classes. This is another
use of the
same keyword. Normally, when we define a generic function, it is independent of
the
data type. The data type will be defined later in the program on calling this function.
The
first line will be as template<generic
data type>. This generic data type is written while
using the class
key word as template<class
variable_name>. So the first line will be as;
template<class T>
We generally use the variable name as
T (T evolves from template).
However, it is not
something hard and fast. After the variable name, we start writing the function
definition.
The function arguments must contain at least one generic data type. Normal function
declaration is:
return_type function_name(argument_list)
return_type can also be of generic type. There should be at least an
argument of generic
type in the argument_list.
Let’s take a very simple example. This is the function
reverse.
It takes one argument and returns its minus version. The
int version of this function
is as:
int reverse(int x)
{
return (-x);
}
Similarly its double version will be as:
double reverse(double x)
{
return (-x);
}
Similarly, we can define it for other data types.
Let’s see how can we make a template of this function. The code is as:
template<class T>
Page 530
T reverse(T x)
{
return (-x);
}
In the above function definition, we have used
T as generic type. The
return type of the
function is T which
is accepting an argument of type
T. In the body of the function,
we
just minus it and return x
that is of type
T. Now in the main program, if we write it as
int i
and then call reverse(i), what
will happen? The compiler will automatically detect that
i
is an int and
reverse is a template
function. So, an int
version of reverse
function is
needed in the program. It uses the template to generate an
int version. How does it
do
that? It replaces the T
with int
in the template function. You will get exactly the same
function as we have written before as
int reverse(int x). This
copy is generated at compile
time. After the compilation, all the code is included in the program. A normal function
call will happen. When we write
reverse(i) in the main
or some other function, it is not
required to tell that an int
version is needed. The compiler will automatically detect the
data type and create a copy of the function of the appropriate data type. This is
important
to understand. Similarly if we have
double y; and we call the
reverse function
as
reverse(y); the compiler will automatically detect that this program
is calling reverse(i)
and reverse(y). Here i
is an int
and in reverse(y), y
is a double.
So the compiler will
generate two versions of reverse
function, one with
int and the other with
double. Then it
will be compiled and the program will execute correctly. This is the classic example
of
code reuse. We have to pay attention to writing the template. It should be generic
in
nature.
For a programmer, there are facilities of the macros and
#define which have the
limitations. Macro is a code substitution while
#define is a value substitution.
Here, in
templates, we write a generic code and the compiler generates its copies of appropriate
types. It is always better than ordinary function overloading. Now let’s take the
previous
example of reverse.
When we write the function of
reverse and give it a value of type
double, a version of the
reverse function for
double is created,
compiled and used. If we
write the same template in some other program and call it for an integer, it will
still work.
It will automatically generate code for
int. We should write a
template while doing the
same functionality with different data types. The rule for templates is that at
least one
argument in the function should be of generic data type. Other wise, it is not a
template
function. We write a template
class T and use
T as a new data type. Being a template data
type, it does not really exist. The compiler will substitute it on its use besides
generating
an appropriate code. There are some limitations that should be kept in mind. We
cannot
store the declarations and definitions of these functions in different files. In
classes, we
have this for certain purposes. In case of a class, we put the declaration of the
class and
its basic structure in the header file to facilitate the users to know what the
class does
implement. The definition of the class, the actual code of its functions and the
manipulations are provided along with as object code. Here in the template case,
the
compiler makes a copy of the source code and converts it to object code. We cannot
give
the declaration of the template function in one file and the definition in some
other. If we
store these in different files, it will not compile. It does not have real data
type and still
Page 531
has parameterized or generic data type in it. So the declaration and definition
of a
template function should be in the same file. We will include this file or keep
the
template with our main program. When it will be used, the copies of code will be
automatically generated. So it is a slight limitation with templates. In any case,
template
class or template functions are for our own use. We do not write template functions
as
libraries for other people as it is like giving away our source code.
For template functions, we must have at least one generic argument. There may be
more
than one generic arguments. We have to pass it to pieces of data to be swapped.
We can
write swap function as:
template<class T>
void swap(T &x, T &y)
{
T tmp;
tmp = x;
x = y;
y = tmp;
}
In the above function, we are passing both arguments of generic type and declared
a tmp
variable of generic type. We can also mix generic data types and native data types
including user defined data types. This template version of swap function can be
used for
integer, double, float or char. Its copy will be created after writing the
swap(a, b) in the
program. If we have int a,
b; in the program,
int copy of swap
function will be generated.
If you have written char a,
b; a char
copy of swap
function will be generated. A copy is
simple substitution of char
with T.
Just replace T with
char in the
swap function and
remove the first line i.e.
template<class T>, this is the function that the compiler will
generate. Now we have seen examples of one generic and two generic arguments
functions. You can write template functions with more than two generic arguments.
So far, we have been using only one generic data type. However, the things can not
be
restricted to only one generic data type. We can use more than one generic data
types in
template functions. We can do that by extending the
template<class T>. The
use of two
generic types can be written as:
template <class T, class U>
We can use any name in place of
T and
U. Two data types can be
mixed here. So we can
write a function that takes an
int and
float and can multiply
these two. We can use T
as int
and U as
float or whatever is the
function requirement. Let’s look at another example of
template function. We want to write a function that takes two arguments of same
type and
tells which of the two is greater. The code will be as below:
// A small program shows the use of template function
Page 532
#include<iostream.h>
// template function of deciding a larger number
template<class T>
T larger(T x, T y)
{
T big;
if (x > y)
big = x;
else
big = y;
return(big);
}
// the main function
void main()
{
int i = 7, j = 12;
double x = 4.5, y = 1.3;
cout << "The larger of " << i << " and " << j << " is " << larger(i, j)<< endl;
cout << "The larger of " << x << " and " << y << " is " << larger(x, y)<< endl;
//cout << "The larger of " << x << " and " << y << " is " << larger(i, y)<< endl;
}
The output of the program is:
The larger of 7 and 12 is 12
The larger of 4.5 and 1.3 is 4.5
The function larger
is very simple. We have two arguments in it, compared these two
arguments and set the variable
bigger. You have noticed
that the definition of larger
is
not exactly correct. In the
if condition, we check that
x is greater than
y. So in the elsepart
x can be equal to or lesser than
y. Let’s see their use
in the main. We declare two
integers i and
j and two doubles
x and
y. Then we use the
cout to display
the result. The
larger function will return the bigger argument. When we write
larger(i, j), the compiler
will detect it and generate an
int version of the larger
function. In the next line, we have
used larger(x, y)
as x and
y are double. Here,
the compiler will generate a double version
of the larger function.
Now compile the program and it is ready to be executed. The two
versions of larger
functions will be executed .We get the larger of two integers and larger
of two doubles. You have noticed that the last line of the code is commented out.
In that
line, we are trying to call the
larger (i, y). There is
a problem that if you uncomment this
line, it will not be compiled. We have only defined one generic class type in the
templatized function i.e. class
T. Here we are trying to call it with an
int and a
double.
The compiler does not know what to do with it. Either it should promote the
int to
double
and call the double
version or demote the
double into int
and call the int
version. But the
Page 533
compiler will not make this decision. It will be a compile time error. We can write
another template function that handles two data types or try to call this function
with one
data type. Be careful about these fine points. Template is a nice thing to use but
needs to
be used carefully. The compiler may give an error, so you have to correctly use
it. Here in
the larger function, we have to provide both arguments of the same data type.
Following is an example of
larger function using two different generic types.
// A template function example using two generic types
#include<iostream.h>
// template function
template <class T, class U>
void larger(T val1, U val2)
{
if (val1 > val2)
cout<<"First is larger"<<endl;
else
cout<<"First is not larger"<<endl;
}
// main function
void main()
{
larger(2.1, 9);
larger('G', ‘A’);
}
The output of the program is:
First is not larger
First is larger
Overloading Template Functions
Let’s take benefit of our knowledge and discuss the things of the next level i.e.
function
overloading. Under the techniques employed in function overloading, the functions
have
the same name but differ either by the number of arguments or the type of the arguments.
Remember that the return type is not a differentiator when you are overloading the
functions. Now if the number or type of the arguments is different and the function
name
is same, the compiler will automatically call the correct version. The same rule
applies to
the template function. We can write overloaded template functions as long as there
is use
of different number or type of arguments.
We have written a templatized
swap function. Let’s rename that function as
inverse. It
will swap the variables. We have another
inverse function that takes
one argument and
return the minus of the argument supplied. We have two template functions named
inverse. Here is the code of the program:
Page 534
// An example of overloaded template functions.
#include<iostream.h>
// template function
template<class T>
void inverse(T &x, T &y)
{
T temp;
temp = x;
x = y;
y = temp;
}
// overloaded inverse fucntion
template<class T>
T inverse(T x)
{
return (-x);
}
// the main fucntion
void main()
{
int i = 3, j = 5;
// calling the templatized functions
inverse(i);
inverse(i, j);
cout << “i = ” << i << ", j = " << j << endl;
}
The output of the program is:
i = 5.6, j = -3.4
In the above program, we have overloaded template functions. When we write
invers(i),
the compiler will detect the
inverse function with one argument and generate its
int code.
However, on writing inverse
(i, j), it will generate an
int version of the
inverse function
which takes two parameters. This is not a good example as the function names are
confusing. The function which does swapping should be named as swap while the one
doing negative should be named as negative. There might be good occasions where
you
might want to use overloaded templates. The same rule of ordinary function overloading
applies on template function overloading.
Explicitly Specifying the Type in a Template Function
Page 535
In the template functions, sometimes we want to see which version of the template
function should be used. Let’s take the example of
reverse function. We call
that function
for double data
type. A function for the double
would have been generated and its
negative value will be returned. Suppose we want to pass it a
double but return an
integer. We want to return a negative integer that was a part of the
double variable,
passed to the function. We can force the compiler to generate an
int version of this
function while not passing it an
int. It can take place
when we are going to call the
function in the program. We write the data type in the angle brackets between the
function name and argument list. For example, if we have a template
reverse function,
which returns -x.
In the program, we have
double a. As soon as we
write reverse(a),
the
compiler will generate a double
version of reverse
function. Now we want that ‘a’ should
be passed to this function while returning an
int. The prototype of the
function is T
reverse(T x). We want that
T should be replaced by
int. At the same
time, we want to pass
it double. To obtain
this, we will write as reverse
<int> (a); writing
<int> forces the
compiler to also generate an integer version of the function. There may be instances
where this technique is useful.
Suppose, we have a template of
reverse function that depends
on two generic data types.
The function template is as follows:
template <class T, class U>
T reverse (U x)
{
return -x;
}
Now the return type is T
while the argument is of type
U. In the body of the function,
we
return -x and the
conversion automatically takes place. In this function template, we are
using two generic types. The return type cannot force anything as it is used later
in the
assignment statement. If we have
double a in the program,
and say reverse(a);
What
version will be generated? What will be replaced with
T and
U? We can force it as
reverse<int>(a); In that case, it will force
T to become
int. It will force
U to become of
the type a i.e.
double. It will
take a double number, reverse and convert it into
int and
return it. You can explicitly specify it as
reverse<int, double> (a);
so we have specified
both T and
U. We are specifying
this to the compiler so that when the compiler generates
the code, it carries out it for these versions. It is like the default argument
list. You can
not force the second part only i.e. you can not force
U only while missing
T. It has to go
left to right. You can do as
revere<double, double> (a); or
reverse(double, int>(a).
The
appropriate versions will be generated. Actually, you can force what type of versions
should be generated in your code. Normally, we do not need to force the template
functions. Normally the template function is used for different data types while
generating appropriate versions by the compiler.
Here is the code of the above-explained program:
// An example of forcing the template functions for some specific data type
Page 536
#include<iostream.h>
template <class T, class U>
T reverse (U x)
{
return (-x);
}
// main function
void main()
{
double amount = -8.8;
// calling the function as double reverse(int)
cout << reverse<double, int>(amount) << endl;
// calling the function as double reverse(double a)
cout << reverse<double>(amount) << endl;
// calling the function as double reverse(double a)
cout << reverse<double, double>(amount) << endl;
// calling the function as int reverse(int a)
cout << reverse<int, int>(amount) << endl;
}
The output of the code is as follows:
8
8.8
8.8
8
Template Functions and Objects
We have seen the template functions and know that classes also have member functions.
Can we use these templates with the member functions of a class? Yes, we can templatize
member functions. The operations used within template functions should be present
in
the public part of the class. Let’s see an example to understand this. Suppose we
have
created a class PhoneCall.
We have a lengthOfCall
data member in the class that tells
about the duration of the call. Another character data member is
billCode. The
billCode
will tell us that this call is local, domestic or international. Suppose, we browse
the bill
and notice a wrong call. What should we do? We will pick up the phone and call the
phone company or go the phone company office to get the bill corrected. How will
they
do that? They will verify it with the record and see there is no such call or the
duration is
not chargeable. So far, we have been using the
reverse function to minus
the input
argument. Here we want to reverse the phone call. Suppose, we define that when the
Page 537
billCode is ‘c’, it means that call has been reversed or cancelled. Can
we use this concept
and write a reverse
function for this class. Let’s revisit the
reverse function template.
template<class T>
T reverse(T x)
{
return (-x);
}
Here T is the generic
type that will be replaced with int, float etc. Can we replace
T with
the object of the class PhoneCall.
How will that work? Let’s replace the
T with
PhoneCall, the code looks like:
PhoneCall reverse(PhoneCall x)
{
return (-x);
}
The declaration line shows that it returns an object of
PhoneCall and takes an
argument
of type PhoneCall.
Inside the body of the function, we are returning
-x. What does
–
PhoneCall mean? When we are using template functions in the classes,
it is necessary to
make sure that whatever usage we are implementing inside the template function,
the
class should support it. Here we want to write
–PhoneCall. So a minus
operator should be
defined for the PhoneCall
class. We know how to define operators for classes. Here the
minus operator for PhoneCall
will change the
billCode to ‘c’ and return the object of type
PhoneCall. Let’s have a look on the code.
// A simple program to show the usage of the template functions in a class
#include<iostream.h>
// reverse template function
template<class T>
T reverse(T x)
{
return (-x);
}
// definition of a class
class PhoneCall
{
private:
int lengthOfCall; // duration of the call
char billCode; // c for cancelled, d for domestic, i for international, l for local
Page 538
public:
PhoneCall(const int l, const char b); // constructor
PhoneCall PhoneCall::operator-(void); // overloaded operator
int getLengthOfCall(){ return lengthOfCall;}
void showCall(void);
};
PhoneCall::PhoneCall(const int len=0, const char b='l')
{
lengthOfCall = len;
billCode = b;
}
void PhoneCall::showCall(void)
{
cout <<"The duration of the call is " << lengthOfCall << endl;
cout <<"The code of the call is " << billCode << endl;
}
// overloaded operator
PhoneCall PhoneCall::operator-(void)
{
PhoneCall::billCode='c';
Return (*this);
}
// main function
void main()
{
PhoneCall aCall(10, 'd');
aCall.showCall();
aCall = reverse(aCall);
aCall.showCall();
}
The output of the code is:
The duration of the call is 10
The code of the call is d
The duration of the call is 10
The code of the call is c
We have overloaded the minus operator in the
PhoneCall class. We cannot
change the
unary or binary nature of operators. The minus operator is lucky due to being unary
as
well as binary simultaneously. Here, we have overloaded unary minus operator so,
it can
be written as -x.
The definition of operators is same as ordinary functions. We can write
whatever is required. Here we are not taking negative of anything but using it in
actual
meaning that is reversing a phone call. In the definition of the minus operator,
we have
Page 539
changed the billCode
to ‘c’ that is cancelled and the object of type
PhoneCall is returned.
Now look at the definition of the
reverse template function
and replace the T
with
PhoneCall. In the body of the function where we have written
–x, the minus operator
of
the PhoneCall class
will be called. Since it is a member operator and the calling object is
available to it. Now let’s look at the main program. We take an object of
PhoneCall as
PhoneCall aCall(10, ‘d’); The object
aCall is initialized through
the constructor. Now we
display it as aCall.showCall();
that shows the length of the call and bill code. After this,
we say reverse(aCall);
The reverse function should not be changing
aCall and should
return an object of type PhoneCall.
It means that aCall = reverse(aCall);
the object
returned is assigned to aCall.
Reverse is a template function, so the compiler will
generate a reverse
function for PhoneCall.
When it will call -x,
the member minus
operator of the class PhoneCall
will be called. It will change the
billCode of that object
and return the object. As we have written
aCall = reverse(aCall)
so the object returned
from reverse having
billCode as ‘c’
will be assigned to aCall.
Now while displaying it
using the aCall.showCall(),
you will see that the billCode
has been changed. So reverse
works.
Recap
Let’s just recap what we just did in this lecture. We have defined a template function
reverse. In the template definition, this function returns
-x whatever
x is passed to it.
After this, we wrote a PhoneCall
class and defined its minus operator. Whenever we have
to take minus of the PhoneCall,
this operator will be called. This action is based on the
phone call domain and is to change the bill code to ‘c’. So the minus operator returns
an
object of type PhoneCall
after changing its bill code. Now these two are independent
exercises. The class PhoneCall
does not know about the
reverse function. It only
has
defined its minus operator. On the other hand, the
reverse template function
has nothing
to do with the PhoneCall
class. It is the main program, linking these two things. The
main function declared an object of type
PhoneCall and called the
reverse function
with
that object. When we write the statement
aCall = reverse (aCall);
the compiler
automatically detects that we have got a situation where
reverse template function
is
called with the object of type
PhoneCall. It needs to
generate a copy of this template
function that will work with
PhoneCall. When it goes to generate that copy, it encounters
with return(-x).
It has to know that a minus operator exists for that class. If we have not
defined the minus operator what will happen. The compiler may give an error that
it does
not know what is -PhoneCall.
On the other hand, sometimes default substitution takes
place. It may not be what you want to do. You have to be careful and look at the
implications of using a template function with a class and make sure all the operations
within template function should be defined explicitly for the class so that function
should
work correctly. Once this is done, you realize that life has become simpler. The
same
reverse function works for this class. Now you can extend the concept
and say how to
reverse a car, how to reverse a phone call, how to reverse an int and so on. The
idea is
combining two very powerful techniques i.e. operator overloading and template
mechanism which provides for writing the code at once. In the normal overloading,
the
facility is that we can use the same name again and again. But we have to write
the code
each time. Normally, the code is different in these overloaded functions. In this
case, we
are saying that we have to write identical code i.e. to reverse something, swap
two things.
Page 540
So the code is same only data type is different then we should go and define a template
for that function and thus, template is used again and again. We started with the
template
function, used at program level. The use of template with class was also demonstrated.
This combination has some rules. This is that all the operations that template function
is
using should be defined in the class. Otherwise, you will have problems.
|
|
|
|