Template Functions

Template Functions video (17 minutes) (Spring 2021)

Programming without Templates

Let's see what C++ programming might be like without templates, using a simple function that calculates the minimum of two things:

#include <iostream>

using std::cout;
using std::endl;

int my_min(int x, int y) {
  return (x < y) ? x : y;
}

int main(int argc, const char *argv[]) {
  cout << my_min(2, 3) << endl;
  cout << my_min(5, 4) << endl;
  return 0;
}

Well that was easy but what if I want my_min to also work on floating point numbers? If I called my_min(2.2, 3.3), it would return 2 instead of 2.2! Let's fix it by overloading the function:

#include <iostream>

using std::cout;
using std::endl;

int my_min(int x, int y) {
  return (x < y) ? x : y;
}

double my_min(double x, double y) {
  return (x < y) ? x : y;
}

int main(int argc, const char *argv[]) {
  cout << my_min(2, 3) << endl;
  cout << my_min(5, 4) << endl;
  cout << my_min(2.2, 3.3) << endl;
  cout << my_min(5.5, 4.4) << endl;
  return 0;
}

Now this works... but it still only works for int and double. What about long? What about std::string? You could just copy and paste the code again and change the types and that will solve the problem for long and std::string. This is a bad solution to my problem: You have to copy-paste the same exact code lots of times and it will only work for the types that you think about. What if someone else creates a new class that's comparable and they want to use my_min? They would have to overload my_min again.

Programming with Templates

Ideally, we'd have some way to write my_min once and have it work for any type. BOOM! Templates.

#include <iostream>
#include <string>

using std::cout;
using std::endl;

template<typename T>
T my_min(const T& x, const T& y) {
  return (x < y) ? x : y;
}

// When you see templates, it's common to use `T` as the typename but you can
// use basically whatever name you want, like you can with variables or classes.
template<typename someTYPEnaaaaame>
someTYPEnaaaaame my_max(const someTYPEnaaaaame& x, const someTYPEnaaaaame& y) {
  return (x > y) ? x : y;
}

int main(int argc, const char *argv[]) {
  string apples = "apples", oranges = "oranges";
  cout << my_min(2, 3) << endl;
  cout << my_min(5, 4) << endl;
  cout << my_min(2.2, 3.3) << endl;
  cout << my_min(5.5, 4.4) << endl;
  cout << my_min(apples, oranges) << endl;
  cout << my_min(oranges, apples) << endl;
  return 0;
}

Now my_min works for almost any type, as long as the less than operator is overloaded for that type (like std::string).

Templates in Header Files

If you accidentally define a function inside a header file and you include that header file more than once, your code won't compile because there will be two version of the same function and the compiler (the linker) won't know which one to use.

bad_my_min.h

#pragma once
// Templates should be declared and defined in the header file.
// If they are declared in the header file and defined in the .cpp file, then
// the compiler won't know how to compile the template when it sees it being
// used in another .cpp file.
template <typename T>
T my_min(const T& x, const T& y);

// my_min should not be defined in the header file because it should only be
// defined/compiled once.
int my_max(int x, int y) {
  return (x > y) ? x : y;
}

// This is correct: non-template functions should be declared but not defined in
// header files.
double my_max(double x, double y);
bad.cpp
#include <iostream>
#include <string>

#include "bad_my_min.h"

using std::cout;
using std::endl;
using std::string;

int main(int argc, const char* argv[]) {
  string apples = "apples", oranges = "oranges";
  cout << "my_min(2, 3) == " << my_min(2, 3) << endl;
  cout << "my_min(5, 4) == " << my_min(5, 4) << endl;
  cout << "my_min(2.2, 3.3) == " << my_min(2.2, 3.3) << endl;
  cout << "my_min(5.5, 4.4) == " << my_min(5.5, 4.4) << endl;
  cout << "my_min(apples, oranges) == " << my_min(apples, oranges) << endl;
  cout << "my_min(oranges, apples) == " << my_min(oranges, apples) << endl;
  cout << endl;
  cout << "my_max(100, 200) == " << my_max(100, 200) << endl;
  cout << "my_max(111.111, 222.222) == " << my_max(111.111, 222.222) << endl;
  return 0;
}
bad_my_min.cpp
#include "bad_my_min.h"

// This template function should be defined in the header file so that when the
// compiler sees this function being used, it can compile it. Since the function
// is defined in this .cpp file and it is not used in this .cpp file, this
// template is never actually used/compiled!
template <typename T>
T my_min(const T& x, const T& y) {
  return (x < y) ? x : y;
}

// This is correct: my_max should be defined in the .cpp file so it's compiled
// exactly once.
double my_max(double x, double y) {
  return (x > y) ? x : y;
}
$ clang++ -pedantic -Wall -lm -std=c++20 -o example bad.cpp bad_my_min.cpp
/usr/bin/ld: /tmp/bad_my_min-a31b70.o: in function `my_max(int, int)':
bad_my_min.cpp:(.text+0x0): multiple definition of `my_max(int, int)'; /tmp/bad-7da94f.o:bad.cpp:(.text+0x0): first defined here
/usr/bin/ld: /tmp/bad-7da94f.o: in function `main':
bad.cpp:(.text+0xe9): undefined reference to `int my_min<int>(int const&, int const&)'
/usr/bin/ld: bad.cpp:(.text+0x164): undefined reference to `int my_min<int>(int const&, int const&)'
/usr/bin/ld: bad.cpp:(.text+0x1f9): undefined reference to `double my_min<double>(double const&, double const&)'
/usr/bin/ld: bad.cpp:(.text+0x292): undefined reference to `double my_min<double>(double const&, double const&)'
/usr/bin/ld: bad.cpp:(.text+0x30a): undefined reference to `std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > my_min<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)'
/usr/bin/ld: bad.cpp:(.text+0x385): undefined reference to `std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > my_min<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)'
clang: error: linker command failed with exit code 1 (use -v to see invocation)

Behind the scenes, when the compiler sees a template function, it doesn't actually compile it right away. Since the compiler doesn't know what type T is until the template is used, it can't compile it until the first time it sees it being used. This means that template functions should be defined in the header file, so that any file that includes the header can see the full template.

good_my_min.h

#pragma once

template <typename T>
T my_min(const T& x, const T& y) {
  return (x < y) ? x : y;
}

int my_max(int x, int y);
double my_max(double x, double y);
good.cpp
#include <iostream>
#include <string>

#include "good_my_min.h"

using std::cout;
using std::endl;
using std::string;

int main(int argc, const char* argv[]) {
  string apples = "apples", oranges = "oranges";
  cout << "my_min(2, 3) == " << my_min(2, 3) << endl;
  cout << "my_min(5, 4) == " << my_min(5, 4) << endl;
  cout << "my_min(2.2, 3.3) == " << my_min(2.2, 3.3) << endl;
  cout << "my_min(5.5, 4.4) == " << my_min(5.5, 4.4) << endl;
  cout << "my_min(apples, oranges) == " << my_min(apples, oranges) << endl;
  cout << "my_min(oranges, apples) == " << my_min(oranges, apples) << endl;
  cout << endl;
  cout << "my_max(100, 200) == " << my_max(100, 200) << endl;
  cout << "my_max(111.111, 222.222) == " << my_max(111.111, 222.222) << endl;
  return 0;
}
good_my_min.cpp
#include "good_my_min.h"

int my_max(int x, int y) {
  return (x > y) ? x : y;
}

double my_max(double x, double y) {
  return (x > y) ? x : y;
}
$ clang++ -pedantic -Wall -lm -std=c++20 -o example good.cpp good_my_min.cpp
$ ./example
my_min(2, 3) == 2
my_min(5, 4) == 4
my_min(2.2, 3.3) == 2.2
my_min(5.5, 4.4) == 4.4
my_min(apples, oranges) == apples
my_min(oranges, apples) == apples

my_max(100, 200) == 200
my_max(111.111, 222.222) == 222.222