Function Templates

Steven J. Zeil

Last modified: Oct 26, 2023
Contents:

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 ints, but not two strings. 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 ints as parameters, the second produces a function named “swap” that takes two doubles as parameters, and the third produces a function named “swap” that takes two strings 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.

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
/***** 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
/***** 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).

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.

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

  1. Copy the whole body into the header file.
  2. Figure out how many template parameters (placeholders) we need and pick names for them.
  3. Add a template header to declare those template parameter names.
  4. 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:

arrayops_templated.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
 */
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.