next up previous contents
Next: Using the MMAB Class Up: C++ for Ocean Modeling Previous: Objects: Operators, and Overloading

Object Templates and Inheritance

Templates and inheritance different things, but they derive from the same principle. The principle is that objects should behave in the same way as similar objects. By inheritance, we say that the new object is just like the old one, except for the following, (whatever it is that is different.) Templates are different in that we don't create a new class, but we do say that we have the same operations being performed in the same way. To illustrate:

template <class T>
class grid2 {
  int nx, ny;
  T grid(nx, ny);

We've declared that there is a grid2 class, that it has integers nx and ny (i.e. the dimensions of our grid), and that it has a grid of data nx by ny of type T. What is this T? T is our templated type. T is any type of thing that the system knows about. It could be an integer, it could be a floating point number, it could be a buoyreport. Logically, we can have grids of all kinds of things. Rather than defining separate classes for each kind of thing that we might grid (a phenomenally tiresome task), and write separate sets of subroutines to operate on each one of them (even worse), we say that we don't care what kind of thing it is that gets gridded. Whatever it is that we've got a grid2 of, we expect to be doing certain things (that's what's in the etc. of the declaration) such as adding two grids of these 'T' things.

Again, this gives us a great savings over Fortran and C. Instead of writing separate routines to find the average of a grid of integers and for a grid of real numbers, we write one routine. The logic is the same after all. Once we write the one routine, we are done. For example, the averaging function for a templated grid is:

template <class T>
T grid2<T>::average() {
  // Note that this says a grid2 of things of type T can be averaged.
  // There are no arguments to this routine.  
  // The result of the averaging operation is another variable of type T
  double sum;  // Make our sum double precision to ensure against overflow
               // in the summation process.
  ijpt loc;    // We'll move over all points in the original grid.

  sum = 0.0;
  for (loc.j = 0; loc.j < this->ypoints() ; loc.j++) {
  for (loc.i = 0; loc.i < this->xpoints() ; loc.i++) {
     sum += this->operator[](loc);
  sum /= (this->ypoints()*this->xpoints() );
  return (T) sum;
The actual implementation is somewhat easier to read than this as we made use of some internal features. This code is robust against changes to the internal representation of grid2's.

Inheritance, I've already mentioned some regarding. Here we'll fill out the picture. We start with our base class, grid2:

template <class T>
class grid2 {
  int nx, ny;
  T grid2(nx, ny);
  T average();
  T maximum();
  T minimum();
  grid2<T> laplace();

  writeout(FILE *);
  readin(FILE *);


The grid2 class I've constructed has quite a few more capabilities than this, but this is illustrative. We can to arithmetic on grid2's, we can read and write them, and we can do a few operations on them like finding the average or taking the laplacean. This is our basic 2d array of things class. It is a templated class, so that the 'things' can be more than the usual integer and floating point as long as we've defined how the operations that grid2's are supposed to be able to do - +,-,*, in this case - work.

Handy as this is, we may want to do more. Often we wind up caring about what point on the earth a grid point corresponds to. In order to do this, we need to know what the map projection is, in full detail. We could, and I did this initially, simply start up a set of classes and say that polars tereographic was descended from the grid2, and a latitude-longitude grids was also descended (separately) from a grid2, and on for each type of map projection. This is legal, but it obscures the fact that each projection has some things in common. Regardless of what the map projection is, we will need two functions: one to convert from ij coordinates to latitude-longitude, and one to convert back. We will also find it useful to import a grid2 to a projection grid. Here we have a case where we know that there are some operations which are required for a proper member of the group, but we don't know ahead of time how to write a function for all of them. This is the case where we want a 'pure virtual' class. The metricgrid is:

template <class T>
class metricgrid : public grid2<T> {
  operator=(grid2 );
  latlonpt locate(ijpt)     = 0 ;
  ijpt     locate(latlonpt) = 0;

We declare that there will be a class called metricgrid, and this is a templated class which inherits from grid2. The first line says that we're going to define how to import a grid2 in to a metricgrid. (We can do this since we can say that it simply means to let the nx, ny, and grid values of the metricgrid be the same as the grid2.) The next two lines are peculiar. The first part is the expectable business of saying that I have two 'locate' functions, one which takes an ijpt and returns a latlonpt, and one which takes a latlonpt and returns the corresponding ijpt. We can't actually write these functions, because we don't know what the map projection is. We put the = 0 in the declaration in order to tell the compiler this. By doing so, we have made metricgrid a virtual class. You can't actually declare an object which is a metricgrid.

What you can do is declare a class which is descended from the metricgrid. In order to construct this class, however, the compiler is going to require that you define how those two locate functions work. For free you get the thousand (and rising) lines of support that exist in the grid2 class, and get to use all functions which know how to use a metricgrid. In repayment, you have to add a couple dozen lines (or whatever) it takes to make the locate functions work. (Well, someone has to write those lines. Preferably we make, say, the ETA people write the ETA locate functions.)

By proper use of these pure virtual classes and some related matters, we also can construct robust libraries and enforce standards. The libraries become robust because we know that certain functions are required of the classes, and can require that they be specified even before knowing who is going to make a new class or for what reason. We enforce standards by virtue of the fact that either: 1) the function or datum is inherited from a standards-conforming base class or 2) the function is required to exist by the base class. We have the added utility in standards enforcement that it becomes much easier to find the right routine to perform the action you want. First, fewer names are required. And second, since the actions are tied to the classes, we know to look in the declaration of the class.

next up previous contents
Next: Using the MMAB Class Up: C++ for Ocean Modeling Previous: Objects: Operators, and Overloading
Robert Grumbine