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
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); }
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.
Contact: webmaster@codeguru.com
CodeGuru - the website for developers.