Cpp Chapter 14: Reusing code in C++ Part2
14.4 Class Templates
) Templates provide parameterized types, which is capable of passing a type name as a recipe for building a class or a function.
) Changes defining a template class:
1 preface class and method definition with
template <class Type>
Here type serves as a generic type specifier(built-in types, classes, etc.)
2 The method name will be altered from
bool Stack::push(const Item & item)
{
...
}
to
bool Stack<Type>::push(const Type & item)
{
...
}
) A particular actualization of a such a template is called instantiation.
Here comes the class definition and class methods file:
// stacktp.h -- a stack template
#ifndef STACKTP_H_INCLUDED
#define STACKTP_H_INCLUDED
template <class Type>
class stack
{
private:
enum {MAX = 10};
Type items[MAX];
int top;
public:
Stack();
bool isempty();
bool isfull();
bool push(const Type & item);
bool pop(Type & item);
};
template <class Type>
Stack<Type>::Stack()
{
top = 0;
}
template <class Type>
bool Stack<Type>::isempty()
{
return top == 0;
}
template <class Type>
bool Stack<Type>::isfull()
{
return top == MAX;
}
template <class Type>
bool Stack<Type>::push(const Type & item)
{
if (top < MAX)
{
items[top++] = item;
return true;
}
else
return false;
}
template <class Type>
bool Stack<Type>::pop(Type & item)
{
if (top > 0)
{
item = items[top--];
return true;
}
else
return false;
}
#endif // STACKTP_H_INCLUDED
) To use template classes, you generate template class in this way:
Stack<int> kernels;
Stack<string> colonels;
Here comes the code:
// stacktem.cpp -- testing the template stack class
#include <iostream>
#include <string>
#include <cctype>
#include "stacktp.h"
using std::cin;
using std::cout;
int main()
{
Stack<std::string> st;
char ch;
std::string po;
cout << "Please enter A to add a purchase order,\n"
<< "P to process a PO, or Q to quit.\n";
while (cin >> ch && std::toupper(ch) != 'Q')
{
while (cin.get() != '\n')
continue;
if (!std::isalpha(ch))
{
cout << '\a';
continue;
}
switch(ch)
{
case 'A':
case 'a':
cout << "Enter a PO number to add: ";
cin >> po;
if (st.isfull())
cout << "stack already full\n";
else
st.push(po);
break;
case 'P':
case 'p':
if (st.isempty())
cout << "stack already empty\n";
else
{
st.pop(po);
cout <<"PO #" << po << " popped\n";
break;
}
}
cout << "Please enter A to add a purchase order,\nP to process a PO, or Q to quit.\n";
}
cout << "Bye\n";
return 0;
}
) To use a stack of pointers to strings, you need to have each pointer point to a separate string to work well.
// stcktp1.h -- modified Stack template
#ifndef STCKTP1_H_INCLUDED
#define STCKTP1_H_INCLUDED
template <class Type>
class Stack
{
private:
enum {SIZE = 10};
int stacksize;
Type * items;
int top;
public:
explicit Stack(int ss = SIZE);
Stack(const Stack & st);
~Stack() {delete [] items;}
bool isempty() {return top == 0;}
bool isfull() {return top == stacksize;}
bool push(const Type & item);
bool pop(Type & item);
Stack & operator=(const Stack & st)
};
template <class Type>
Stack<Type>::Stack(int ss) : stacksize(ss), top(0)
{
items = new Type [stacksize];
}
template <class Type>
Stack(Type)::Stack(const Stack & st)
{
stacksize = st.stacksize;
top = st.top;
items = new Type [stacksize];
for (int i = 0; i < top; i++)
items[i] = st.items[i];
}
template <class Type>
bool Stack<Type>::push(const Type & item)
{
if (top < stacksize)
{
items[top++] = item;
return true;
}
else
return false;
}
template <class Type>
bool Stack<Type>::pop(Type & item)
{
if (top > 0)
{
item = items[--top];
return true;
}
else
return false;
}
template <class Type>
Stack<Type> & Stack<Type>::operator=(const Stack<Type> & st)
{
if (this = &st)
return *this;
delete [] items;
stacksize = st.stacksize;
top = st.top;
items = new Type[stacksize];
for (int i = 0; i < top; i++)
items[i] = st.items[i];
return *this;
}
#endif // STCKTP1_H_INCLUDED
Noteworthy:
The prototype for assignment operator declared the return type of the method as Stack rather than Stack
Stack & operator=(const Stack & st);
This is an abbreviation used only in class scope.
) Here comes the code that uses the simplified version of stack:
// stkoptr1.cpp -- testing stack of pointers
#include <iostream>
#include <cstdlib>
#include <ctime>
#include "stcktp1.h"
const int Num = 10;
int main()
{
std::srand(std::time(0));
std::cout << "Please enter stack size: ";
int stacksize;
std::cin >> stacksize;
Stack<const char *> st(stacksize);
const char * in[Num] = {"a","b","c","d","e","f","g","h","i","j"};
const char * out[Num];
int processed = 0;
int nextin = 0;
while (processed < Num)
{
if (st.isempty())
st.push(in[nextin++]);
else if (st.isfull())
st.pop(out[processed++]);
else if (std::rand() % 2 && nextin < Num)
st.push(in[nextin++]);
else
st.pop(out[processed++]);
}
for (int i = 0; i < Num; i++)
std::cout << out[i] << std::endl;
std::cout << "Bye\n";
return 0;
}
) Next let's come to an array template example:
// arraytp.h -- Array Template
#ifndef ARRAYTP_H_INCLUDED
#define ARRAYTP_H_INCLUDED
#include <iostream>
#include <cstdlib>
template <class T, int n>
class ArrayTP
{
private:
T ar[n];
public:
ArrayTp() {}
explicit ArrayTP(const T & v);
virtual T & operator[] (int i);
virtual T operator[] (int i) const;
};
template <class T, int n>
ArrayTP<T,n>::ArrayTP(const T & v)
{
for (int i = 0; i < n; i++)
ar[i] = v;
}
template <class T, int n>
T & ArrayTP<T,n>::operator[](int i)
{
if (i < 0 || i >= n)
{
std::cerr << "Error in array limits: " << i << " is out of range\n";
std::exit(EXIT_FAILUER);
}
return ar[i];
}
template <class T, int n>
T ArrayTP<T,n>::operator[](int i) const
{
if (i < 0 || i >= n)
{
std::cerr << "Error in array limits: " << i << " is out of range\n";
std::exit(EXIT_FAILURE);
}
return ar[i];
}
#endif // ARRAYTP_H_INCLUDED
Noteworthy
1 Note the template heading:
template <class T, int n>
This second parameter is called a non-type, as it acts as a particular type. You can use the template in this way:
ArrayTP<double, 12> eggweights;
This initializes an array of 12 doubles called eggweights.
2 Advantage and drawback
Advantage: fast execution, free from new and delele
Drawback: Each array size generate its own template
) As normal classes, you could also use template classes for inheritance or containment.
You could also use templates recursively:
ArrayTP< ArrayTP<int,5>, 10> twodee;
This is the same effect as
int twodee[10][5];
Here comes an example using the recursive call of template to create a two-dimensional array:
template <class T, int n>
class ArrayTP
{
private:
T ar[n];
public:
ArrayTP() {};
explicit ArrayTP(const T & v);
virtual T & operator[](int i);
virtual T operator[](int i) const;
};
) You could also use templates with different types combined:
template <class T1, class T2>
Here comes the code using this trick with a Pair class:
// pairs.cpp -- defining and using a Pair template
#include <iostream>
#include <string>
template <class T1, class T2>
class Pair
{
private:
T1 a;
T2 b;
public:
T1 & first();
T2 & second();
T1 first() const { return a;}
T2 second() const { return b;}
Pair(const T1 & aval, const T2 & bval) : a(aval), b(bval) {}
Pair() {}
};
template <class T1, class T2>
T1 & Pair<T1,T2>::first()
{
return a;
}
template <class T1, class T2>
T2 & Pair<T1,T2>::second()
{
return b;
}
int main()
{
using std::cout;
using std::endl;
using std::string;
Pair<string,int> ratings[4] =
{
Pair<string,int>("a",1),
Pair<string,int>("b",2),
Pair<string,int>("c",3),
Pair<string,int>("d",4),
};
int joints = sizeof(ratings) / sizeof(Pair<string,int>);
cout << "Rating:\t Eatery\n";
for (int i = 0; i < joints; i++)
cout << ratings[i].second() << ":\t " << ratings[i].first() << endl;
cout << "Oops! Revised rating:\n";
ratings[3].first() = "g";
ratings[3].second() = 6;
cout << ratings[3].second() << ":\t " << ratings[3].first() << endl;
return 0;
}
) You can provide default values for type parameters:
template <class T1, class T2 = int> class Topo {...}
) Template specializations
1 Implicit instantiation
When an object of the desired type is declared, the compiler generates a specialized class definition, using the recipe provided by the template:
ArrayTP<double, 30> * pt;
pt = new ArrayTP<double, 30>;
The compiler only generates a class for ArrayTP<double, 30> at the second sentence because only the second sentence requires declaration of a specific object of the class ArrayTP<double, 30>.
2 Explicit instantiation
When you explicitly use the keyword template and desired type to declare a class, the compiler generates one with the specific data types according to the template instructions even if there is no object of the class needed currently:
template class ArrayTP<string, 100>;
3 Explicit specialization
In some of the cases, the template's methods might work differently for one specific type, so you might want to single out the desired type's method implementations. A specialized class template definition has the following form:
template <> class Classname<specialized-type-name> { ... };
for example, after you explicit specialized template <> class Thing<char *> { ... }
, next time you require a class of the char * type, the compiler would use the specialized version rather than generating one by the template.
) Member templates
A template could be a member of a structure, class or template class:
// tempmemb.cpp -- template members
#include <iostream>
using std::cout;
using std::endl;
template <typename T>
class beta
{
private:
template <typename V>
class hold
{
private:
V val;
public:
hold(V v = 0) : val(v) {}
void show() const {cout << val << endl;}
V Value() const {return val;}
};
hold<T> q;
hold<int> n;
public:
beta(T t, int i) : q(t), n(i) {}
template<typename U>
U blab(U u, T t) { return (n.Value() + q.Value()) * u / t;}
void Show() const { q.show(); n.show();}
};
int main()
{
beta<double> guy(3.5, 3);
cout << "T was set to double\n";
guy.Show();
cout << "V was set to T, which is double, then V was set to int\n";
cout << guy.blab(10,2.3) << endl;
cout << "U was set to int\n";
cout << guy.blab(10.0, 2.0) << endl;
cout << "U was set to double\n";
cout << "Done\n";
return 0;
}
) Templates could also be parameters for templates:
template <template <typename T> class Thing>
Here template <typename T> class
is the type and Thing is the parameter. View an example:
// tempparm.cpp -- templates as parameters
#include <iostream>
#include "stacktp.h"
template <template <typename T> class Thing>
class Crab
{
private:
Thing<int> s1;
Thing<double> s2; // Thing is the template parameter
public:
Crab() {};
bool push(int a, double x) { return s1.push(a) && s2.push(x);}
bool pop(int & a, double & x) { return s1.pop(a) && s2.pop(x);}
};
int main()
{
using std::cout;
using std::cin;
using std::endl;
Crab<Stack> nebula; // send Stack template as parameter to template class Crab
int ni;
double nb;
cout << "Enter int double pairs, such as 4 3.5 (0 0 to end):\n";
while (cin >> ni >> nb && ni > 0 && nb > 0)
{
if (!nebula.push(ni,nb))
break;
}
while (nebula.pop(ni,nb))
cout << ni << ", " << nb <<endl;
cout << "Done.\n";
return 0;
}
) Template classes and friends
1 Non-template friend functions to template classes
This means that the function is friend to all possible instantiations of the tempalate, use this form:
template <class T>
class HasFriend
{
friend void report(HasFriend<T> &);
...
};
In this case report could accept a object of specific type as parameter.
Here comes an example:
// frnd2tmp.cpp -- template class with non-template friends
#include <iostream>
using std::cout;
using std::endl;
template <typename T>
class HasFriend
{
private:
T item;
static int ct;
public:
HasFriend(const T & i) : item(i) {ct++;}
~HasFriend() { ct--; }
friend void counts();
friend void reports(HasFriend<T> &);
};
template <typename T>
int HasFirned<T>::ct = 0;
void counts()
{
cout << "int count: " << HasFriend<int>::ct << "; ";
cout << "double conut: " << HasFriend<double>::ct << "\n";
}
void reports(HasFriend<int> & hf)
{
cout << "HasFriend<int>: " << hf.item << endl;
}
void reports(HasFriend<double> & hf)
{
cout << "HasFriend<double>: " << hf.item << endl;
}
int main()
{
cout << "No objects declared: ";
counts();
HasFriend<int> hfi1(10);
cout << "After hfi1 declared: ";
counts();
HasFriend<int> hfi2(20);
cout << "After hfi2 declared: ";
counts();
HasFriend<double> hfdb(10.5);
cout << "After hfdb declared: ";
counts();
reports(hfi1);
reports(hfi2);
reports(hfdb);
return 0;
}
2 Bound template friend functions to template classes
This means that the type of the friend is determined by the type of class when it is instantiated.
3 Unbound template friend functions to template classes
This means that all specializations of the friend are friends to each specialization of the class.