CS202 Lecture Notes - Templates

Constructs That Make Life "Easier"

As you start programming bigger and better things, you will find parts of your programming language to be constricting. Depending on the language, there will be some constructs that are defined to make life less constricting. The following constructs in C and C++ fit this description: All of these have similar plusses and minuses -- the plusses are generality, flexibility and sometimes efficiency. The major minus is readability, for all of these, and sometimes they handcuff your code structure and you end up making bad decisions. For that reason I always caution students to think carefully before launching into any of these.

To highlight this, I worked with a colleague who was an excellent programmer, and who decided that templates would add flexibility to our project. At first, it was great, but as the software developed, the template required us to duplicate code, which led to bugs. As it turns out, interfaces were the far preferable solution, and we ended up ripping out the templates and reimplementing. That's part of life, but it is also a cautionary tale. Think twice.

On the flip side, the Standard Template Library is a beautiful use of templates that most definitely improves functionality, readbility and reusability.

So let's learn.


The Basics

Templates allow you to define a procedure, class or method that employs data whose type is flexible. Sometimes it will work with any type, and sometimes it will work with specific classes of types. Let's show the latter. This code comes by way of Dr. Gregor, BTW. In the program src/add_flex.cpp, I define a procedure add() which uses a template for its parameters and its return value:

template <typename T>
T add(const T &v1, const T &v2)        // This code returns the "sum" of its arguments,
{                                      // and it works on any type where '+' is defined.
  return v1 + v2; 
}

The "template" line says that the procedure may use the type named T as a placeholder for whatever type will be used. So long as the type being used has the '+' operator defined, you may call add() on instances of the type.

Here's a main() that calls add() three times -- once on integers, once on doubles and once on strings:

/* Our main will first read two integers and print their sum,
   then two doubles and print their sum,
   then two strings and print their sum. */

int main() 
{
  int i, j;
  double x, y;
  string s, t;

  cout << "Enter two integers: " ;
  cin >> i >> j;
  cout << "Their sum is " << add(i, j) << endl << endl;

  cout << "Enter two doubles: " ;
  cin >> x >> y;
  cout << "Their sum is " << add(x, y) << endl << endl;

  cout << "Enter two strings: " ;
  cin >> s >> t;
  cout << "Their sum is " << add(s, t) << endl << endl;

  return 0;
}

We run it, and as you can see, it has added quite a bit of flexibility to our code -- the proper '+' is called for each data type:

UNIX> bin/add_flex
Enter two integers: 4 9
Their sum is 13                  # Here, add() is called on integers.

Enter two doubles: 3.4 5.5
Their sum is 8.9                 # Here, add() is called on doubles.

Enter two strings: Jim Plank
Their sum is JimPlank            # Here, add() is called on strings.

UNIX> 
If you want to treat a type in a special manner, you can do so -- in src/add_flex_s.cpp, we add a special procedure for strings that puts a space in the middle.

template <typename T>
T add(const T &v1, const T &v2)        // Same code as before.
{                                      
  return v1 + v2; 
}

                                       // We add this code to treat strings differently
                                       // from other types:
template <>
string add(const string &v1, const string &v2) 
{ 
  return v1 + " " + v2; 
}

UNIX> bin/add_flex_s
Enter two integers: 4 9
Their sum is 13

Enter two doubles: 3.4 5.5
Their sum is 8.9

Enter two strings: Jim Plank      # Here the special code is called for strings.
Their sum is Jim Plank

UNIX> 
I'll be honest -- I don't use templates, but I can see how their usage above can simplify code that does the same thing, but on different numerical types.

Using templates to hold generic data

The standard template library uses templates as a way to hold generic data. This is why, for example, vectors, deques and lists can hold anything, and sets/maps can work on any data type that defines comparison.

Let's show an example of how one might implement a more generic data structure with templates. In include/heap_one.hpp, I define (and implement) a Heap data structure that uses a template so that it can store any data type where '<' and '>' are defined:

#pragma once
#include <vector>

template <typename T>
class Heap {
  public:
    void    Push(const T &d);
    T       Pop();
    size_t  Size() const;
    bool    Empty() const;

  protected:
    std::vector <T> h;
};

I have two programs that use this data structure to print the 5 minimum elements on standard input. One prints integers, and the other prints strings:

src/h1_min_5_int.cpp

#include <iostream>
#include "heap_one.hpp"
using namespace std;

/* This uses the templated Heap data structure 
   from heap_one.hpp to print the
   five smallest integers on standard input. */

int main()
{
  Heap <int> h;
  int i;

  while (cin >> i) h.Push(i);

  for (i = 0; i < 5 && !h.Empty(); i++) {
    cout << h.Pop() << endl;
  }
  return 0;
}
src/h1_min_5_str.cpp

#include <iostream>
#include "heap_one.hpp"
using namespace std;

/* This uses the templated Heap data structure 
   from heap_one.hpp to print the
   five smallest strings on standard input. */

int main()
{
  Heap <string> h;
  string s;
  int i;

  while (getline(cin, s)) h.Push(s);

  for (i = 0; i < 5 && !h.Empty(); i++) {
    cout << h.Pop() << endl;
  }
  return 0;
}

That's really convenient, isn't it? One unfortunate thing, however, is that I have to implement the data structure in the header file. The reason has to do with compilation. I won't go into it -- if you want all of the pedantry and condescension that you can handle, read about it on stack overflow.

Here, for example, is how Size() is implemented -- you have to define the template, and put the template into the class specification:

template <typename T>
size_t Heap<T>::Size() const  { return h.size(); }

As always, I put the implementation separate from the class defintion to make the definition clean and easy to read.


Making the heap data structure more useful

The Heap implemented above shares a problem with the priority_queue data structure of the standard template library. If you want to store an instance of a class, you have to define the '<'/'>' operators on the class. (In the STL, you can also pass a comparison function).

Let's change our heap so that it holds a key and a value, like maps and multimaps do. We'll do it a little differently -- when you Push() you specify and key and a val, and when you Pop() you get the val that corresponds to the minimum key. Here's the definition of the class, in include/heap_two.hpp

#pragma once
#include <vector>
#include <string>

template <typename Key, typename Val>
class Heap {
  public:
    void    Push(const Key &k, const Val &v);
    Val     Pop();
    size_t  Size() const;
    bool    Empty() const;

  protected:
    std::vector < std::pair<Key, Val > > h;
};

/* The methods are implemented below... */

template <typename Key, typename Val>
size_t Heap<Key,Val>::Size() const  { return h.size(); }

/* You can see the rest in the file. */

Now, the program src/h2_min_5_people.cpp reads "people" on standard input, where a person has a first name, last name and id. It prints the people with the five minimum id's. In my opinion, this implementation of the Heap is much more usable.

#include <iostream>
#include <cstdio>
#include "heap_two.hpp"
using namespace std;

/* We're going to read in "people", where a person has a
   first name, a last name, and an id number.  It will
   print the people with the five smallest id numbers. */

class Person
{
  public:
    string fn;
    string ln;
    int id;
};

int main()
{
  Heap <int, Person *> h;
  Person *p;
  int i;
  string fn, ln;

  while (cin >> i >> fn >> ln) {
    p = new Person;
    p->fn = fn;
    p->ln = ln;
    p->id = i;
    h.Push(p->id, p);
  }

  for (i = 0; i < 5 && !h.Empty(); i++) {
    p = h.Pop();
    printf("%-15s %-15s %8d\n", p->fn.c_str(), p->ln.c_str(), p->id);
  }
  return 0;
}

Here it is running on txt/people.txt, which contains 10,000 random "people":

UNIX> wc txt/people.txt
   10000   30000  229492 txt/people.txt
UNIX> head txt/people.txt
9392853 Zachary Idiotic
6597179 Makayla Walkout
9023575 Jordan Fredrickson
8147031 Jake Snatch
8585074 Charlotte Tray
3524143 Sophia Died
4851306 Jake Sly
2647509 Lucas Rough
4640885 James Babysit
4436205 Ella Barbital
UNIX> sort -n < txt/people.txt | head -n 5
160 Oliver Diameter
571 Sophia Bowie
753 Aiden Pagan
1721 Aaron Cinnamon
3143 Makayla Transient
UNIX> bin/h2_min_5_people < txt/people.txt
Oliver          Diameter             160
Sophia          Bowie                571
Aiden           Pagan                753
Aaron           Cinnamon            1721
Makayla         Transient           3143
UNIX> 

Summary

Templates allow for greater flexibility in your programs. At their best, they let you define data structures that hold arbitrary (or near-arbitrary) data types. I know I haven't done a deep dive into templates here, but I believe I've given you enough to get started with templates if you are interested.

I know I'm a broken record here, but you really need to think twice before putting templates into your code. While they do allow for flexibility, they also expose you to classes of bugs that are easy to make and hard to find.