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:

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:

console shot

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.