MFC Programmer's SourceBook : Thinking in C++
Bruce Eckel's Thinking in C++, 2nd Ed Contents | Prev | Next

Function templates

A class template describes an infinite set of classes, and the most common place you’ll see templates is with classes. However, C++ also supports the concept of an infinite set of functions, which is sometimes useful. The syntax is virtually identical, except that you create a function instead of a class.

The clue that you should create a function template is, as you might suspect, if you find you’re creating a number of functions that look identical except that they are dealing with different types. The classic example of a function template is a sorting function. [50] However, a function template is useful in all sorts of places, as demonstrated in the first example that follows. The second example shows a function template used with containers and iterators.

A memory allocation system

There’s a few things you can do to make the raw memory allocation routines malloc( ), calloc( ) and realloc( ) safer. The following function template produces one function getmem( ) that either allocates a new piece of memory or resizes an existing piece (like realloc( )). In addition, it zeroes only the new memory , and it checks to see that the memory is successfully allocated. Also, you only tell it the number of elements of the type you want, not the number of bytes, so the possibility of a programmer error is reduced. Here’s the header file:

//: C16:Getmem.h
// Function template for memory
#ifndef GETMEM_H
#define GETMEM_H
#include <cstdlib>
#include <cstring>
#include "../require.h"

template<class T>
void getmem(T*& oldmem, int elems) {
  typedef int cntr; // Type of element counter
  const int csz = sizeof(cntr); // And size
  const int tsz = sizeof(T);
  if(elems == 0) {
    free(&(((cntr*)oldmem)[-1]));
    return;
  }
  T* p = oldmem;
  cntr oldcount = 0;
  if(p) { // Previously allocated memory
    // Old style:
    // ((cntr*)p)--; // Back up by one cntr
    // New style:
    cntr* tmp = reinterpret_cast<cntr*>(p);
    p = reinterpret_cast<T*>(--tmp);
    oldcount = *(cntr*)p; // Previous # elems
  }
  T* m = (T*)realloc(p, elems * tsz + csz);
  require(m != 0);
  *((cntr*)m) = elems; // Keep track of count
  const cntr increment = elems - oldcount;
  if(increment > 0) {
    // Starting address of data:
    long startadr = (long)&(m[oldcount]);
    startadr += csz;
    // Zero the additional new memory:
    memset((void*)startadr, 0, increment * tsz);
  }
  // Return the address beyond the count:
  oldmem = (T*)&(((cntr*)m)[1]);
}

template<class T>
inline void freemem(T * m) { getmem(m, 0); }

#endif // GETMEM_H ///:~ 

To be able to zero only the new memory, a counter indicating the number of elements allocated is attached to the beginning of each block of memory. The typedef cntr is the type of this counter; it allows you to change from int to long if you need to handle larger chunks (other issues come up when using long, however – these are seen in compiler warnings).

A pointer reference is used for the argument oldmem because the outside variable (a pointer) must be changed to point to the new block of memory. oldmem must point to zero (to allocate new memory) or to an existing block of memory that was created with getmem( ). This function assumes you’re using it properly, but for debugging you could add an additional tag next to the counter containing an identifier, and check that identifier in getmem( ) to help discover incorrect calls.

If the number of elements requested is zero, the storage is freed. There’s an additional function template freemem( ) that aliases this behavior.

You’ll notice that getmem( ) is very low-level – there are lots of casts and byte manipulations. For example, the oldmem pointer doesn’t point to the true beginning of the memory block, but just past the beginning to allow for the counter. So to free( ) the memory block, getmem( ) must back up the pointer by the amount of space occupied by cntr. Because oldmem is a T*, it must first be cast to a cntr*, then indexed backwards one place. Finally the address of that location is produced for free( ) in the expression:

free(&(((cntr*)oldmem)[-1]));

Similarly, if this is previously allocated memory, getmem( ) must back up by one cntr size to get the true starting address of the memory, and then extract the previous number of elements. The true starting address is required inside realloc( ). If the storage size is being increased, then the difference between the new number of elements and the old number is used to calculate the starting address and the amount of memory to zero in memset( ). Finally, the address beyond the count is produced to assign to oldmem in the statement:

oldmem = (T*)&(((cntr*)m)[1]);

Again, because oldmem is a reference to a pointer, this has the effect of changing the outside argument passed to getmem( ).

Here’s a program to test getmem( ). It allocates storage and fills it up with values, then increases that amount of storage:

//: C16:Getmem.cpp
// Test memory function template
#include <iostream>
#include "Getmem.h"
using namespace std;

int main() {
  int* p = 0;
  getmem(p, 10);
  for(int i = 0; i < 10; i++) {
    cout << p[i] << ' ';
    p[i] = i;
  }
  cout << '\n';
  getmem(p, 20);
  for(int j = 0; j < 20; j++) {
    cout << p[j] << ' ';
    p[j] = j;
  }
  cout << '\n';
  getmem(p, 25);
  for(int k = 0; k < 25; k++)
    cout << p[k] << ' ';
  freemem(p);
  cout << '\n';

  float* f = 0;
  getmem(f, 3);
  for(int u = 0; u < 3; u++) {
    cout << f[u] << ' ';
    f[u] = u + 3.14159;
  }
  cout << '\n';
  getmem(f, 6);
  for(int v = 0; v < 6; v++)
    cout << f[v] << ' ';
  freemem(f);
} ///:~ 

After each getmem( ), the values in memory are printed out to show that the new ones have been zeroed.

Notice that a different version of getmem( ) is instantiated for the int and float pointers. You might think that because all the manipulations are so low-level you could get away with a single non-template function and pass a void*& as oldmem. This doesn’t work because then the compiler must do a conversion from your type to a void*. To take the reference, it makes a temporary. This produces an error because then you’re modifying the temporary pointer, not the pointer you want to change. So the function template is necessary to produce the exact type for the argument.

Applying a function to a TStack

Suppose you want to take a TStack and apply a function to all the objects it contains. Because a TStack can contain any type of object, you need a function that works with any type of TStack and any type of object it contains:

//: C16:Applist.cpp
// Apply a function to a TStack
#include <iostream>
#include "TStack.h"
using namespace std;

// 0 arguments, any type of return value:
template<class T, class R>
void applist(TStack<T>& tl, R(T::*f)()) {
  TStackIterator<T> it(tl);
  while(it) {
    (it.current()->*f)();
    it++;
  }
}

// 1 argument, any type of return value:
template<class T, class R, class A>
void applist(TStack<T>& tl, R(T::*f)(A), A a) {
  TStackIterator<T> it(tl);
  while(it) {
    (it.current()->*f)(a);
    it++;
  }
}

// 2 arguments, any type of return value:
template<class T, class R, class A1, class A2>
void applist(TStack<T>& tl, R(T::*f)(A1, A2),
    A1 a1, A2 a2) {
  TStackIterator<T> it(tl);
  while(it) {
    (it.current()->*f)(a1, a2);
    it++;
  }
}

// Etc., to handle maximum probable arguments

class Gromit { // The techno-dog
  int arf;
public:
  Gromit(int arf = 1) : arf(arf + 1) {}
  void speak(int) {
    for(int i = 0; i < arf; i++)
      cout << "arf! ";
    cout << endl;
  }
  char eat(float) {
    cout << "chomp!" << endl;
    return 'z';
  }
  int sleep(char, double) {
    cout << "zzz..." << endl;
    return 0;
  }
  void sit(void) {}
};

int main() {
  TStack<Gromit> dogs;
  for(int i = 0; i < 5; i++)
    dogs.push(new Gromit(i));
  applist(dogs, &Gromit::speak, 1);
  applist(dogs, &Gromit::eat, 2.0f);
  applist(dogs, &Gromit::sleep, 'z', 3.0);
  applist(dogs, &Gromit::sit);
} ///:~ 

The applist( )function template takes a reference to the container class and a pointer-to-member for a function contained in the class. It uses an iterator to move through the Stack and apply the function to every object. If you’ve (understandably) forgotten the pointer-to-member syntax, you can refresh your memory at the end of Chapter 9.

You can see there is more than one version of applist( ), so it’s possible to overload function templates. Although they all take any type of return value (which is ignored, but the type information is required to match the pointer-to-member), each version takes a different number of arguments, and because it’s a template, those arguments can be of any type. (You can see different sets of functions in class Gromit.)[51] The only limitation here is that there’s no “super template” to create templates for you; thus you must decide how many arguments will ever be required.

Although the definition of applist( ) is fairly complex and not something you’d ever expect a novice to understand, its use is remarkably clean and simple, and a novice could easily use it knowing only what it is intended to accomplish, not how. This is the type of division you should strive for in all of your program components: The tough details are all isolated on the designer’s side of the wall, and users are concerned only with accomplishing their goals, and don’t see, know about, or depend on details of the underlying implementation

Of course, this type of functionality is strongly tied to the TStack class, so you’d normally find these function templates in the header file along with TStack.

Member function templates

It’s also possible to make applist( ) a member function template of the class. That is, a separate template definition from the class’ template, and yet a member of the class. Thus, you can end up with the cleaner syntax:

dogs.applist(&Gromit::sit);

This is analogous to the act (in Chapter 1) of bringing ordinary functions inside a class. [52]


[50] See C++ Inside & Out (Osborne/McGraw-Hill, 1993) by the author, Chapter 10.

[51] A reference to the British animated short The Wrong Trousers by Nick Park.

[52] Check your compiler version information to see if it supports member function templates.

Contents | Prev | Next


Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru