Function Templates
Steven J. Zeil
We’ve suggested that programs may have many instances of code that are really minor variations on the same “pattern”.
These code instances can be created manually by programmers by editing an existing version of the code, making changes for each desired variation on the pattern. But this process can be tedious and error-prone.
1 Function Templates
That’s where templates come in. A C++ template is a kind of pattern for code. Within that pattern are certain names that, like T
in the earlier example, we intend to replace by something “real” when we use the pattern. These names are called template parameters.
The compiler instantiates (creates an instance of) a template by filling in the appropriate replacement for these template parameter names, thereby generating the actual code for a function or class, and compiling that instantiated code.
2 First Templates: swap, min, & max
Let’s start with a very simple pattern for code that you have probably written many times: exchanging the values of two variables.
Have you ever written code that looked like:
// Exchange the values of x and y
int temp = x;
x = y;
y = temp;
or
// Exchange the values of s1 and s2
string temp = s1;
s1 = s2;
s2 = temp;
or maybe something similar for exchanging floating point numbers, or characters, or …? This is a perfect example of a pattern for code that pretty much remains the same regardless of what the actual data types being used might be. (More specifically, the source code that we write would look the same. The compiler may need to generate very differnet code for copying, say, strings, than it would generate for copying integers.)
It’s a good candidate for a template. First, we think about hwo we might capture this code in a function:
void swap (int& x, int& y)
{
int temp = x;
x = y;
y = temp;
}
This function would work for swapping the values of two int
s, but not two string
s. But we can imagine writing a whole slew of functions of the form:
void swap (T& x, T& y)
{
T temp = x;
x = y;
y = temp;
}
thinking of T
as a placeholder for the “real” type. This isn’t legal C++, though, because, T
doesn’t stand for anything. But we can turn this pattern for a funciton into a template by adding a header that tells the compiler that we want this to be a template and that T is a placeholder:
template <typename T>
void swap (T& x, T& y)
{
T temp = x;
x = y;
y = temp;
}
Voila! We have a template. We can now write
int i = ...
int j = ...
double x = ...
double y = ...
string s = ...
string t = ...
⋮
swap(i, j);
swap(x, y);
swap(s, t);
Each time we use swap
, the compiler looks at the actual data types of the parameters being passed, and if we have never used that particular combination of data types before, generates a new function using our swap
pattern. The technical terminology here is that the compiler instantiates the swap
template to match the actual parameter types. So the first call to swap will produce a function named “swap” that takes two int
s as parameters, the second produces a function named “swap” that takes two double
s as parameters, and the third produces a function named “swap” that takes two string
s as parameters.
Let’s look at another couple of common programming patterns:
// Set z to the smaller of x and y
z = x;
if (y < x)
z = y;
and
// Set z to the larger of x and y
z = x;
if (x < y)
z = y;
Again, we can easily think of functions for this purpose…
T min (const T& x, const T& y)
{
T z = x;
if (y < x)
z = y;
return z;
}
T max (const T& x, const T& y)
{
T z = x;
if (x < y)
z = y;
return z;
}
or, even shorter
T min (const T& x, const T& y)
{
return (x < y) ? x : y;
}
T max (const T& x, const T& y)
{
return (x < y) ? y : x;
}
Now, in this case, we never said what the data types of x
and y
were in the original pattern, so using a placeholder T
seems natural. And all we need to do is to add the headers to get true C++ templates:
template <typename T>
T min (const T& x, const T& y)
{
return (x < y) ? x : y;
}
template <typename T>
T max (const T& x, const T& y)
{
return (x < y) ? y : x;
}
These should work for any data types that support the <
operator. So
string s = min("abc", "def"); // returns "abc"
double d = max(-1.0, 1.0); // returns 1.0
are fine but
struct Point {
double x;
double y
};
Point p1 = ...
Point p2 = ...
Point p = min(p1, p2); // error
will produce a compilation error because we have no ‘<’ operator on the data type Point
.
-
We don’t need to use “T” as our placeholder. For example, I like to use
template <typename Comparable> Comparable min (const Comparable& x, const Comparable& y) { return (x < y) ? x : y; }
for placeholders that will need a comparison operator (
<
) to work. -
More complex templates may need multiple placeholders. We can have as many placeholders as we need by separating them with commas inside the “
<
>
”, e.g.,template <typename T, typename U>
The key restriction is that each placeholder must appear somewhere within the function’s formal parameter list.
The three templates we have shown here, swap
, min
, and max
, are actually all part of the C++ standard library, in the <algorithm>
header.
3 Writing Function Templates
For examples of writing some larger templates, let’s look at the functions for manipulating arrays that we used in the previous reading:
/***** arrayops.h *****/
#ifndef ARRAYOPS_H
#define ARRAYOPS_H
/**
*
* Assume the elements of the array are already in order
* Find the position where value could be added to keep
* everything in order, and insert it there.
* Return the position where it was inserted
* - Assumes that we have a separate integer (size) indicating how
* many elements are in the array
* - and that the "true" size of the array is at least one larger
* than the current value of that counter
*
* @param array array into which to add an element
* @param size number of data elements in hte array. Must be less than
* the number of elements allocated for the array. Incremented
* upon output from this function.
* @param value value to add into the array
* @return the position where the element was added
*/
int addInOrder (int* array, int& size, int value);
/*
* Search an array for a given value, returning the index where
* found or -1 if not found.
*
* From Malik, C++ Programming: From Problem Analysis to Program Design
*
* @param list the array to be searched
* @param listLength the number of data elements in the array
* @param searchItem the value to search for
* @return the position at which value was found, or -1 if not found
*/
int seqSearch(const int list[], int listLength, int searchItem);
/*
* Search an ordered array for a given value, returning the index where
* found or -1 if not found.
* @param list the array to be searched. Must be ordered.
* @param listLength the number of data elements in the array
* @param searchItem the value to search for
* @return the position at which value was found, or -1 if not found
*/
int seqOrderedSearch(const int list[], int listLength, int searchItem);
/*
* Removes an element from the indicated position in the array, moving
* all elements in higher positions down one to fill in the gap.
*
* @param array array from which to remove an element
* @param size number of data elements in the array. Decremented
* upon output from this function.
* @param index position from which to remove the element. Must be < size
*/
void removeElement (int* array, int& size, int index);
/**
* Performs the standard binary search using two comparisons per level.
* Returns index where item is found or -1 if not found
*
* From Weiss, Data Structures and Algorithm Analysis, 4e
* ( modified SJ Zeil)
*
* @param a array to search. Must be ordered.
* @param size number of elements i nthe array
* @param x value to search for
* @return position where found or -1 if not found
*/
int binarySearch( const int* a, int size, const int & x );
#endif
/***** arrayops.cpp *****/
#include "arrayops.h"
/**
*
* Assume the elements of the array are already in order
* Find the position where value could be added to keep
* everything in order, and insert it there.
* Return the position where it was inserted
* - Assumes that we have a separate integer (size) indicating how
* many elements are in the array
* - and that the "true" size of the array is at least one larger
* than the current value of that counter
*
* @param array array into which to add an element
* @param size number of data elements in hte array. Must be less than
* the number of elements allocated for the array. Incremented
* upon output from this function.
* @param value value to add into the array
* @return the position where the element was added
*/
int addInOrder (int* array, int& size, int value)
{
// Make room for the insertion
int toBeMoved = size - 1;
while (toBeMoved >= 0 && value < array[toBeMoved]) {
array[toBeMoved+1] = array[toBeMoved];
--toBeMoved;
}
// Insert the new value
array[toBeMoved+1] = value;
++size;
return toBeMoved+1;
}
/*
* Search an array for a given value, returning the index where
* found or -1 if not found.
*
* From Malik, C++ Programming: From Problem Analysis to Program Design
*
* @param list the array to be searched
* @param listLength the number of data elements in the array
* @param searchItem the value to search for
* @return the position at which value was found, or -1 if not found
*/
int seqSearch(const int list[], int listLength, int searchItem)
{
int loc;
for (loc = 0; loc < listLength; loc++)
if (list[loc] == searchItem)
return loc;
return -1;
}
/*
* Search an ordered array for a given value, returning the index where
* found or -1 if not found.
* @param list the array to be searched. Must be ordered.
* @param listLength the number of data elements in the array
* @param searchItem the value to search for
* @return the position at which value was found, or -1 if not found
*/
int seqOrderedSearch(const int list[], int listLength, int searchItem)
{
int loc = 0;
while (loc < listLength && list[loc] < searchItem)
{
++loc;
}
if (loc < listLength && list[loc] == searchItem)
return loc;
else
return -1;
}
/*
* Removes an element from the indicated position in the array, moving
* all elements in higher positions down one to fill in the gap.
*
* @param array array from which to remove an element
* @param size number of data elements in the array. Decremented
* upon output from this function.
* @param index position from which to remove the element. Must be < size
*/
void removeElement (int* array, int& size, int index)
{
int toBeMoved = index + 1;
while (toBeMoved < size) {
array[toBeMoved] = array[toBeMoved+1];
++toBeMoved;
}
--size;
}
const int NOT_FOUND = -1;
/**
* Performs the standard binary search using two comparisons per level.
* Returns index where item is found or -1 if not found
*
* From Weiss, Data Structures and Algorithm Analysis, 4e
* ( modified SJ Zeil)
*
* @param a array to search. Must be ordered.
* @param size number of elements i nthe array
* @param x value to search for
* @return position where found or -1 if not found
*/
int binarySearch( const int* a, int size, const int & x )
{
int low = 0, high = a.size( ) - 1;
while( low <= high )
{
int mid = ( low + high ) / 2;
if( a[ mid ] < x )
low = mid + 1;
else if( a[ mid ] > x )
high = mid - 1;
else
return mid; // Found
}
return NOT_FOUND; // NOT_FOUND is defined as -1
}
These are all written to manipulate arrays of int
, so we need to generalize these to manipulate arrays of any type T
(or some other placeholder name).
- Be careful. Some of the “int”s in that code represent the data being stored in the array, but some really are integers (e.g., the
size
of the array).
Before we move forward with that step, we need to consider one other interesting aspect of templates. Think of how we are going to use, for example, the template form of
int seqSearch(const int list[], int listLength, int searchItem);
We might think that it will look somehting like
template <typename Comparable>
int seqSearch(const Comparable list[], int listLength, Comparable searchItem);
so that we could write code like
#include "arrayops.h"
...
string* words = new string[100];
...
int position = seqSearch(words, numWords, "foo");
When the compiler sees this call matching the seqSearch
pattern, it should generate the actual code for an array-of-strings version of seqSeach
.
- The pattern of a templates is instantiated when we compile uses of that template.
- We are used to dividing our functions into declarations, in a header file, that are used when we compile code that calls those functions, and definitions, in a compilation unit (.cpp file) that are brought in when we link all of our compiled code to form an executable.
- For normal code, the compiler doesn’t need to see the bodies of the functions in order to generate the code required to call them.
- For calls to templated functions, however, the compiler needs to see the templated function body when compiling the calls, so that it can instantiate the code for that particular version of the template.
As a consequence,
we put the whole body of the template into a header file
so that it gets seen (via #include
) during the compilation phase.
So, to templatize seqSearch
, we
- Copy the whole body into the header file.
- Figure out how many template parameters (placeholders) we need and pick names for them.
- Add a template header to declare those template parameter names.
- Use those template parameter names in place of the original data type that we are trying to generalize away.
So, seqSearch
becomes
template <typename Comparable>
int seqSearch(const Comparable list[], int listLength, Comparable searchItem)
{
int loc;
for (loc = 0; loc < listLength; loc++)
if (list[loc] == searchItem)
return loc;
return -1;
}
And the rest of our functions can be generalized similarly:
#ifndef ARRAYOPS_H
#define ARRAYOPS_H
/**
*
* Assume the elements of the array are already in order
* Find the position where value could be added to keep
* everything in order, and insert it there.
* Return the position where it was inserted
* - Assumes that we have a separate integer (size) indicating how
* many elements are in the array
* - and that the "true" size of the array is at least one larger
* than the current value of that counter
*
* @param array array into which to add an element
* @param size number of data elements in hte array. Must be less than
* the number of elements allocated for the array. Incremented
* upon output from this function.
* @param value value to add into the array
* @return the position where the element was added
*/
template <typename Comparable>
int addInOrder (Comparable* array, int& size, Comparable value)
{
// Make room for the insertion
int toBeMoved = size - 1;
while (toBeMoved >= 0 && value < array[toBeMoved]) {
array[toBeMoved+1] = array[toBeMoved];
--toBeMoved;
}
// Insert the new value
array[toBeMoved+1] = value;
++size;
return toBeMoved+1;
}
/*
* Search an array for a given value, returning the index where
* found or -1 if not found.
*
* From Malik, C++ Programming: From Problem Analysis to Program Design
*
* @param list the array to be searched
* @param listLength the number of data elements in the array
* @param searchItem the value to search for
* @return the position at which value was found, or -1 if not found
*/
template <typename T>
int seqSearch(const T list[], int listLength, T searchItem)
{
int loc;
for (loc = 0; loc < listLength; loc++)
if (list[loc] == searchItem)
return loc;
return -1;
}
/*
* Search an ordered array for a given value, returning the index where
* found or -1 if not found.
* @param list the array to be searched. Must be ordered.
* @param listLength the number of data elements in the array
* @param searchItem the value to search for
* @return the position at which value was found, or -1 if not found
*/
template <typename Comparable>
int seqOrderedSearch(const Comparable list[], int listLength,
Comparable searchItem)
{
int loc = 0;
while (loc < listLength && list[loc] < searchItem)
{
++loc;
}
if (loc < listLength && list[loc] == searchItem)
return loc;
else
return -1;
}
/*
* Removes an element from the indicated position in the array, moving
* all elements in higher positions down one to fill in the gap.
*
* @param array array from which to remove an element
* @param size number of data elements in the array. Decremented
* upon output from this function.
* @param index position from which to remove the element. Must be < size
*/
template <typename T>
void removeElement (T* array, int& size, int index)
{
int toBeMoved = index + 1;
while (toBeMoved < size) {
array[toBeMoved] = array[toBeMoved+1];
++toBeMoved;
}
--size;
}
/**
* Performs the standard binary search using two comparisons per level.
* Returns index where item is found or -1 if not found
*
* From Weiss, Data Structures and Algorithm Analysis, 4e
* ( modified SJ Zeil)
*
* @param a array to search. Must be ordered.
* @param size number of elements i nthe array
* @param x value to search for
* @return position where found or -1 if not found
*/
template <typename Comparable>
int binarySearch( const Comparable* a, int size, const Comparable & x )
{
const int NOT_FOUND = -1;
int low = 0, high = a.size( ) - 1;
while( low <= high )
{
int mid = ( low + high ) / 2;
if( a[ mid ] < x )
low = mid + 1;
else if( a[ mid ] > x )
high = mid - 1;
else
return mid; // Found
}
return NOT_FOUND; // NOT_FOUND is defined as -1
}
#endif
In early versions of C++ templates, a template parameter that stood for a data type was introduced by the keyword “class” instead of “typename
”. That’s still accepted (and fairly common.1 However, some people felt that it was a bad choice because sometimes the parameter (T) will wind up standing for a primitive non-class type (e.g., int). So later versions of C++ recommended the use of the newer keyword “typename” instead. Both are acceptable. Both mean exactly the same thing.
4 Function Templates and the C++ std Library
The C++ std
library has a number of useful templates for small, common programming idioms, many in the header <algorithm>
. We’ve already seen swap
, min
, and max
.
Some others of note are
4.1 fill_n
To fill an array of size n
with a constant value:
template <typename T>
void fill_n (T* array, int n, T value)
{
for (int i = 0; i < n; ++i)
array[i] = value;
}
Not exactly earth-shaking, but often convenient.
4.2 relops
namespace relops {
template <typename T>
inline bool operator!= (const T& a, const T& b)
{
return !(a == b);
}
template <typename T>
inline bool operator> (const T& a, const T& b)
{
return b < a;
}
template <typename T>
inline bool operator<= (const T& a, const T& b)
{
return !(b < a);
}
template <typename T>
inline bool operator>= (const T& a, const T& b)
{
return !(a < b);
}
}
Because of these templates, if we want a new class to have a full set of relational operators, we only need to provide <
and ==
. The std::relops
templates take care of the other 4:
class PersonnelRecord {
public:
PersonnelRecord (const std::string& name, ...);
⋮
bool operator<
(const PersonnelRecord&) const;
bool operator==
(const PersonnelRecord&) const;
};
⋮
using namespace std::relops; // Without this, the relops templates
// aren't visible to our code.
⋮
if (myRecord >= yourRecord) // OK, defined in terms of <
4.2.1 Coming up…
We’ll see some other std
templates later, after we introduce the iterator
ADT.
1: You may see it a lot in my code, because I’ve been programming in C++ for a long time, and old habits are hard to break.