Generic Classes. McGraw-Hill The Complete Reference Fourth Edition
Werbung
Werbung
472
C + + : T h e C o m p l e t e R e f e r e n c e cout << str[i] << ' '; cout << endl; compact(nums, 7, 2, 4); compact(str, 18, 6, 10); cout << "Here is compacted integer array: "; for(i=0; i<7; i++) cout << nums[i] << ' '; cout << endl; cout << "Here is compacted string: "; for(i=0; i<18; i++) cout << str[i] << ' '; cout << endl;
} return 0;
This program compacts two different types of arrays. One is an integer array, and the other is a string. However, the compact( ) function will work for any type of array. The output from this program in shown here.
Here is uncompacted integer array: 0 1 2 3 4 5 6
Here is uncompacted string: G e n e r i c F u n c t i o n s
Here is compacted integer array: 0 1 5 6 0 0 0
Here is compacted string: G e n e r i c t i o n s
As the preceding examples illustrate, once you begin to think in terms of templates, many uses will naturally suggest themselves. As long as the underlying logic of a function is independent of the data, it can be made into a generic function.
Generic Classes
In addition to generic functions, you can also define a generic class. When you do this, you create a class that defines all the algorithms used by that class; however, the actual type of the data being manipulated will be specified as a parameter when objects of that class are created.
Generic classes are useful when a class uses logic that can be generalized. For example, the same algorithms that maintain a queue of integers will also work for a queue of characters, and the same mechanism that maintains a linked list of mailing
C h a p t e r 1 8 : T e m p l a t e s
473 addresses will also maintain a linked list of auto part information. When you create a generic class, it can perform the operation you define, such as maintaining a queue or a linked list, for any type of data. The compiler will automatically generate the correct type of object, based upon the type you specify when the object is created.
The general form of a generic class declaration is shown here: template <class Ttype> class class-name {
.
.
.
}
Here, Ttype is the placeholder type name, which will be specified when a class is instantiated. If necessary, you can define more than one generic data type using a comma-separated list.
Once you have created a generic class, you create a specific instance of that class using the following general form:
class-name <type> ob;
Here, type is the type name of the data that the class will be operating upon. Member functions of a generic class are themselves automatically generic. You need not use
template
to explicitly specify them as such.
In the following program, the stack class (first introduced in Chapter 11) is reworked into a generic class. Thus, it can be used to store objects of any type. In this example, a character stack and a floating-point stack are created, but any data type can be used.
// This function demonstrates a generic stack.
#include <iostream> using namespace std; const int SIZE = 10;
// Create a generic stack class template <class StackType> class stack {
StackType stck[SIZE]; // holds the stack int tos; // index of top-of-stack public: stack() { tos = 0; } // initialize stack void push(StackType ob); // push object on stack
474
C + + : T h e C o m p l e t e R e f e r e n c e
};
StackType pop(); // pop object from stack
// Push an object.
template <class StackType> void stack<StackType>::push(StackType ob)
{ if(tos==SIZE) { cout << "Stack is full.\n"; return;
} stck[tos] = ob; tos++;
}
// Pop an object.
template <class StackType> StackType stack<StackType>::pop()
{ if(tos==0) { cout << "Stack is empty.\n"; return 0; // return null on empty stack
} tos--; return stck[tos];
} int main()
{
// Demonstrate character stacks.
stack<char> s1, s2; // create two character stacks int i; s1.push('a'); s2.push('x'); s1.push('b'); s2.push('y'); s1.push('c'); s2.push('z'); for(i=0; i<3; i++) cout << "Pop s1: " << s1.pop() << "\n"; for(i=0; i<3; i++) cout << "Pop s2: " << s2.pop() << "\n";
// demonstrate double stacks stack<double> ds1, ds2; // create two double stacks
C h a p t e r 1 8 : T e m p l a t e s
475 ds1.push(1.1); ds2.push(2.2); ds1.push(3.3); ds2.push(4.4); ds1.push(5.5); ds2.push(6.6); for(i=0; i<3; i++) cout << "Pop ds1: " << ds1.pop() << "\n"; for(i=0; i<3; i++) cout << "Pop ds2: " << ds2.pop() << "\n";
} return 0;
As you can see, the declaration of a generic class is similar to that of a generic function. The actual type of data stored by the stack is generic in the class declaration.
It is not until an object of the stack is declared that the actual data type is determined.
When a specific instance of stack is declared, the compiler automatically generates all the functions and variables necessary for handling the actual data. In this example, two different types of stacks are declared. Two are integer stacks. Two are stacks of doubles.
Pay special attention to these declarations: stack<char> s1, s2; // create two character stacks stack<double> ds1, ds2; // create two double stacks
Notice how the desired data type is passed inside the angle brackets. By changing the type of data specified when stack objects are created, you can change the type of data stored in that stack. For example, by using the following declaration, you can create another stack that stores character pointers.
stack<char *> chrptrQ;
You can also create stacks to store data types that you create. For example, if you want to use the following structure to store address information, struct addr { char name[40]; char street[40]; char city[30]; char state[3];
476
C + + : T h e C o m p l e t e R e f e r e n c e
}; char zip[12]; then to use stack to generate a stack that will store objects of type addr, use a declaration like this: stack<addr> obj;
As the stack class illustrates, generic functions and classes are powerful tools that you can use to maximize your programming efforts, because they allow you to define the general form of an object that can then be used with any type of data. You are saved from the tedium of creating separate implementations for each data type with which you want the algorithm to work. The compiler automatically creates the specific versions of the class for you.
An Example with Two Generic Data Types
A template class can have more than one generic data type. Simply declare all the data types required by the class in a comma-separated list within the template specification.
For example, the following short example creates a class that uses two generic data types.
/* This example uses two generic data types in a class definition.
*/
#include <iostream> using namespace std; template <class Type1, class Type2> class myclass
{
Type1 i;
Type2 j; public: myclass(Type1 a, Type2 b) { i = a; j = b; }
}; void show() { cout << i << ' ' << j << '\n'; } int main()
{ myclass<int, double> ob1(10, 0.23); myclass<char, char *> ob2('X', "Templates add power.");
C h a p t e r 1 8 : T e m p l a t e s
477 ob1.show(); // show int, double ob2.show(); // show char, char *
} return 0;
This program produces the following output:
10 0.23
X Templates add power.
The program declares two types of objects. ob1 uses int and double data. ob2 uses a character and a character pointer. For both cases, the compiler automatically generates the appropriate data and functions to accommodate the way the objects are created.
Applying Template Classes: A Generic Array Class
To illustrate the practical benefits of template classes, let's look at one way in which they are commonly applied. As you saw in Chapter 15, you can overload the [ ] operator. Doing so allows you to create your own array implementations, including
"safe arrays" that provide run-time boundary checking. As you know, in C++, it is possible to overrun (or underrun) an array boundary at run time without generating a run-time error message. However, if you create a class that contains the array, and allow access to that array only through the overloaded [ ] subscripting operator, then you can intercept an out-of-range index.
By combining operator overloading with a template class, it is possible to create a generic safe-array type that can be used for creating safe arrays of any data type. This type of array is shown in the following program:
// A generic safe array example.
#include <iostream>
#include <cstdlib> using namespace std; const int SIZE = 10; template <class AType> class atype {
AType a[SIZE]; public: atype() {
478
C + + : T h e C o m p l e t e R e f e r e n c e
}; register int i; for(i=0; i<SIZE; i++) a[i] = i;
}
AType &operator[](int i);
// Provide range checking for atype.
template <class AType> AType &atype<AType>::operator[](int i)
{ if(i<0 || i> SIZE-1) { cout << "\nIndex value of "; cout << i << " is out-of-bounds.\n"; exit(1);
} return a[i];
} int main()
{ atype<int> intob; // integer array atype<double> doubleob; // double array int i; cout << "Integer array: "; for(i=0; i<SIZE; i++) intob[i] = i; for(i=0; i<SIZE; i++) cout << intob[i] << " "; cout << '\n'; cout << "Double array: "; for(i=0; i<SIZE; i++) doubleob[i] = (double) i/3; for(i=0; i<SIZE; i++) cout << doubleob[i] << " "; cout << '\n'; intob[12] = 100; // generates runtime error
} return 0;
This program implements a generic safe-array type and then demonstrates its use by creating an array of ints and an array of doubles. You should try creating other types of arrays. As this example shows, part of the power of generic classes is that they
C h a p t e r 1 8 : T e m p l a t e s
479 allow you to write the code once, debug it, and then apply it to any type of data without having to re-engineer it for each specific application.
Using Non-Type Arguments with Generic Classes
In the template specification for a generic class, you may also specify non-type arguments. That is, in a template specification you can specify what you would normally think of as a standard argument, such as an integer or a pointer. The syntax to accomplish this is essentially the same as for normal function parameters: simply include the type and name of the argument. For example, here is a better way to implement the safe-array class presented in the preceding section. It allows you to specify the size of the array.
// Demonstrate non-type template arguments.
#include <iostream>
#include <cstdlib> using namespace std;
// Here, int size is a non-type argument.
template <class AType, int size> class atype {
AType a[size]; // length of array is passed in size public: atype() { register int i; for(i=0; i<size; i++) a[i] = i;
};
}
AType &operator[](int i);
// Provide range checking for atype.
template <class AType, int size>
AType &atype<AType, size>::operator[](int i)
{ if(i<0 || i> size-1) { cout << "\nIndex value of "; cout << i << " is out-of-bounds.\n"; exit(1);
} return a[i];
} int main()
{
480
C + + : T h e C o m p l e t e R e f e r e n c e atype<int, 10> intob; // integer array of size 10 atype<double, 15> doubleob; // double array of size 15 int i; cout << "Integer array: "; for(i=0; i<10; i++) intob[i] = i; for(i=0; i<10; i++) cout << intob[i] << " "; cout << '\n'; cout << "Double array: "; for(i=0; i<15; i++) doubleob[i] = (double) i/3; for(i=0; i<15; i++) cout << doubleob[i] << " "; cout << '\n'; intob[12] = 100; // generates runtime error
} return 0;
Look carefully at the template specification for atype. Note that size is declared as an int. This parameter is then used within atype to declare the size of the array a. Even though size is depicted as a "variable" in the source code, its value is known at compile time. This allows it to be used to set the size of the array. size is also used in the bounds checking within the operator[ ]( ) function. Within main( ), notice how the integer and floating-point arrays are created. The second parameter specifies the size of each array.
Non-type parameters are restricted to integers, pointers, or references. Other types, such as float, are not allowed. The arguments that you pass to a non-type parameter must consist of either an integer constant, or a pointer or reference to a global function or object. Thus, non-type parameters should themselves be thought of as constants, since their values cannot be changed. For example, inside operator[ ]( ), the following statement is not allowed.
size = 10; // Error
Since non-type parameters are treated as constants, they can be used to set the dimension of an array, which is a significant, practical benefit.
As the safe-array example illustrates, the use of non-type parameters greatly expands the utility of template classes. Although the information contained in the non-type argument must be known at compile-time, this restriction is mild compared with the power offered by non-type parameters.
C h a p t e r 1 8 : T e m p l a t e s
481
Using Default Arguments with Template Classes
A template class can have a default argument associated with a generic type.
For example, template <class X=int> class myclass { //...
Here, the type int will be used if no other type is specified when an object of type
myclass
is instantiated.
It is also permissible for non-type arguments to take default arguments. The default value is used when no explicit value is specified when the class is instantiated. Default arguments for non-type parameters are specified using the same syntax as default arguments for function parameters.
Here is another version of the safe-array class that uses default arguments for both the type of data and the size of the array.
// Demonstrate default template arguments.
#include <iostream>
#include <cstdlib> using namespace std;
// Here, AType defaults to int and size defaults to 10.
template <class AType=int, int size=10> class atype {
AType a[size]; // size of array is passed in size public: atype() { register int i; for(i=0; i<size; i++) a[i] = i;
};
}
AType &operator[](int i);
// Provide range checking for atype.
template <class AType, int size>
AType &atype<AType, size>::operator[](int i)
{ if(i<0 || i> size-1) { cout << "\nIndex value of "; cout << i << " is out-of-bounds.\n"; exit(1);
}
482
C + + : T h e C o m p l e t e R e f e r e n c e
} return a[i]; int main()
{ atype<int, 100> intarray; // integer array, size 100 atype<double> doublearray; // double array, default size atype<> defarray; // default to int array of size 10 int i; cout << "int array: "; for(i=0; i<100; i++) intarray[i] = i; for(i=0; i<100; i++) cout << intarray[i] << " "; cout << '\n'; cout << "double array: "; for(i=0; i<10; i++) doublearray[i] = (double) i/3; for(i=0; i<10; i++) cout << doublearray[i] << " "; cout << '\n'; cout << "defarray array: "; for(i=0; i<10; i++) defarray[i] = i; for(i=0; i<10; i++) cout << defarray[i] << " "; cout << '\n';
} return 0;
Pay close attention to this line: template <class AType=int, int size=10> class atype {
Here, AType defaults to type int, and size defaults to 10. As the program illustrates,
atype
objects can be created three ways:
■ explicitly specifying both the type and size of the array
■ explicitly specifying the type, but letting the size default to 10
■ letting the type default to int and the size default to 10
C h a p t e r 1 8 : T e m p l a t e s
483
The use of default arguments—especially default types—adds versatility to your template classes. You can provide a default for the type of data most commonly used while still allowing the user of your classes to specialize them as needed.
Explicit Class Specializations
As with template functions, you can create an explicit specialization of a generic class.
To do so, use the template<> construct, which works the same as it does for explicit function specializations. For example:
// Demonstrate class specialization.
#include <iostream> using namespace std; template <class T> class myclass {
T x; public: myclass(T a) {
}; cout << "Inside generic myclass\n"; x = a;
}
T getx() { return x; }
// Explicit specialization for int.
template <> class myclass<int> { int x; public: myclass(int a) { cout << "Inside myclass<int> specialization\n"; x = a * a;
};
} int getx() { return x; } int main()
{ myclass<double> d(10.1); cout << "double: " << d.getx() << "\n\n"; myclass<int> i(5);
Herunterladen
Werbung