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.
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.
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.
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>
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.