Section 6 - A Step Toward Templates
1B.6.1 A Plan for Templates
One thing we will do in this class is create our own class templates, and for that reason, we review how that's done.
We have seen how one defines function templates, and class templates are the analogous, both in purpose and syntax. The purpose is to have a set of encapsulated methods and data without committing to any particular data type. You may have done this sort of thing with base and derived classes. For instance, have you ever created a generic Node type that does all the calisthenics needed for linked lists, but does not have data in it -- only a next pointer? If so, you might have derived from this class and in the derived class defined your private member data which gets added to the base class Node equipment. That's one way to approach things.
Class templates represent a different approach. With templates we will actually store the data in our basic Node class, the catch being, the data type is non-committal - it takes the form of a type parameter, just as function templates used type parameters. Then, when we want to use the class template, we don't derive from it as we did with a base Node class. Rather we instantiate (or specialize) an object immediately, passing the desired data type to the template.
Enough theory, let's see how it is done.
1B.6.2 A Class That Emulates an Array
We are trying to create a float array that is better than this:
float rainfall[5];
By better, I mean one that will not do bad things if we try to assign a number to rainfall[8], beyond the end of the array. The fact that we can overload the bracket operator, [], is going to make our solution a satisfying one. Here is the class prototype:
class SafeArray { static const long MAX_ARRAY_SIZE = 100000L; float *data; long size; public: SafeArray(long sz = 100, float initVal = 0); SafeArray(const SafeArray & rhs); ~SafeArray(); float &operator[](long index); SafeArray &operator=(const SafeArray & rhs); };
As you can see, the private data of the class consists of a float * which tells us that the array will be dynamically allocated. That means our destructor needs to do clean up, and we also should supply a copy constructor and overload the assignment operator.
1B.6.3 Reference Return Types
In what follows, the logic is straightforward. The only things that are worth mentioning are the return types of the brackets operator, [], and the assignment operator. Both have reference (&) return types. A reference return type enables a function to be on the LHS (left hand side) of an assignment operator. That's because we are returning the address of some variable, which means we can assign a value to it. Just remember to always use the & for return types of operator[] and operator=.
1B.6.4 SafeArray Implementation
Here is the code for the definitions of these member functions.
// SafeArray method definitions ------------------- SafeArray::SafeArray(long sz, float initVal) { if (sz < 1) size = 1; else if (sz > MAX_ARRAY_SIZE) size = MAX_ARRAY_SIZE; else size = sz; data = new float[size]; for (int k = 0; k < size; k++) data[k] = initVal; } SafeArray::SafeArray(const SafeArray& rhs) : data(NULL) { *this = rhs; } SafeArray::~SafeArray() { delete[] data; } float &SafeArray::operator [](long index) { static float staticBuff; if (index < 0 || index >= size) return staticBuff; else return data[index]; } SafeArray &SafeArray::operator=(const SafeArray & rhs) { // always check this if (this == &rhs) return (*this); delete[] data; // clear old data size = rhs.size; data = new float[size]; for (int k=0; k < size; k++) data[k] = rhs.data[k]; return *this; }
This is not a complete class. I have simply provided the protection against bounds violations without a good error reporting mechanism. To make it more complete, we could include an error flag, error value, and/or exception. We keep it simple for now, if a bit unrealistic.
Besides the key aspect of this class -- the way in which the operator[] method protects against out-of-bounds errors, making the class absolutely safe -- this example demonstrates how a heap-based class handles its primary methods. Here are some highlights:
- The copy constructor must allocate memory based on the input parameter and copy the data to that new heap memory. However, the common way to do this is to call the assignment operator so as to avoid code duplication. We must be careful to initialize our data to NULL first, otherwise, the assignment operator call will die on delete[] data;.
- The assignment operator should allocate memory based on the input parameter and copy the data to that new heap memory. This allows one array to be assigned to another using the = and have the desired effect that all the element data will be copied (something that doesn't normally happen with arrays). We must always remember to first test for this == &rhs to avoid a client accidentally clobbering an object with a call like x = x;.
- The brackets operator returns a reference so that expressions like array[k] can appear on the LHS (left hand side) of the = sign. It must also supply a static object to return in case of an out-of-bounds error.
We'll test the array with this main():
int main() { int k; SafeArray rainfall(5); // assign data and go out-of-bounds for (k = -3; k < 10; k++) rainfall[k] = 17 + .03 * k; cout << "The array after some assignments:\n"; for (k = -3; k < 10; k++) cout << rainfall[k] << endl; // test the assignment operator SafeArray weather(40); weather = rainfall; cout << "Did weather[2] get assigned rainfall[2]?\n"; cout << weather[2] << " " << rainfall[2] << endl; return 0; }
which produces this output:

You may be tempted to set the return staticBuff of the brackets operator to 0 so that out-of-bounds accesses would return something neutral, like 0. However this will not carry over well to templates, so we don't do it. We are satisfied that our SafeArray protects against array bounds.
OurSafeArrayclass seems to work. Now that we have solved this problem for float arrays, we want to create a solution for all other data types. Enter class templates.