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. [56] 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 string conversion system

//: C19:stringConv.h
// Chuck Allison's string converter
#ifndef STRINGCONV_H
#define STRINGCONV_H
#include <string>
#include <sstream>

template<typename T>
T fromString(const std::string& s) {
  std::istringstream is(s);
  T t;
  is >> t;
  return t;
}

template<typename T>
std::string toString(const T& t) {
  std::ostringstream s;
  s << t;
  return s.str();
}
#endif // STRINGCONV_H ///:~

Here’s a test program, that includes the use of the Standard Library complex number type:

//: C19:stringConvTest.cpp
#include "stringConv.h"
#include <iostream>
#include <complex>
using namespace std;

int main() {
  int i = 1234;
  cout << "i == \"" << toString(i) << "\"\n";
  float x = 567.89;
  cout << "x == \"" << toString(x) << "\"\n";
  complex<float> c(1.0, 2.0);
  cout << "c == \"" << toString(c) << "\"\n";
  cout << endl;
  
  i = fromString<int>(string("1234"));
  cout << "i == " << i << endl;
  x = fromString<float>(string("567.89"));
  cout << "x == " << x << endl;
  c = fromString< complex<float> >(string("(1.0,2.0)"));
  cout << "c == " << c << endl;
} ///:~

The output is what you’d expect:

i == "1234"
x == "567.89"
c == "(1,2)"

i == 1234
x == 567.89
c == (1,2)

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:

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

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:

//: C19:Getmem.cpp
// Test memory function template
#include "Getmem.h"
#include <iostream>
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.


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

Contents | Prev | Next


Contact: webmaster@codeguru.com
CodeGuru - the website for developers.
[an error occurred while processing this directive]