15: Templates

UC Irvine - Fall ‘22 - ICS 45C

Quick list of things I want to talk about:

  • Function templates
  • Class templates

Expanded notes:

So far we’ve been creating functions and structures that work with a single type. For example, we worked on a LinkedList that stores only ints, and when we talked about functions, we had to overload the function (i.e., create many different definitions) to handle parameters from different types.

However, there is a better way to allow your code to work with many types: templates. A template defines how your code should work for a generic type T, and when a user wants to use it, they will specify what’s the type and use it.

This will make more sense with examples, so let’s take a look at a few!

Function template

We’ll start off with function templates. You might remember when we created functions like this:

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

Then, if we wanted our code to work with floats, we would define a 2nd version:

float findMax(float x, float y) {
  return (x > y) ? x : y;
}

That seems wasteful, we’re copy-pasting pretty much the same code, just changing the types.

When you have something like this, you can define a template. The syntax is as follows:

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

Before creating our function, we have a new line that says template <typename T>. This line indicates that the next definition is not an actual function, but a template that’s using T as a placeholder for a type. It doesn’t have to be always T, it can be any valid name over there. The required syntax is just template <typename ?>, where you put a name you want instead of ?.

When using this syntax, you’re not actually creating a function, but a template instead. Because of that, a user cannot use:

cout << findMax(2, 5);

since findMax is not a function. Instead, the user needs to specify what type they’re expecting to use:

cout << findMax<int>(2, 5);

This is done with <type> like in the example above. You can imagine that <> is like a “template call”, where you’re giving an argument to that placeholder we had previously defined.

Class template

Similarly to functions, it might be useful to make templates out of classes. The process is very similar, we would add the template <typename T> directive above the class, and then replace the types that would change with T.

As an example, let’s revisit our LinkedList class that we had implemented in the previous set of notes. That class/struct combo stores only ints, and if we wanted to store other types, we would need to create other But now that we know about templates, we can modify the code to account for that.

So, we can define our struct and class like this:

template <typename T>
struct LinkedListNode {
  T value;
  struct LinkedListNode* next;
};

template <typename T>
class LinkedList {
 private:
  const std::string name;
  LinkedListNode<T>* head;
  LinkedListNode<T>* tail;

 public:
  LinkedList();
  LinkedList(std::string name);
  ~LinkedList();
  void addToFront(T x);
  void addToBack(T x);
  void printList() const;
  friend std::ostream& operator<<(std::ostream& os, const LinkedList& ll) {
    LinkedListNode<T>* ptr = ll.head;
    os << "Values inside '" << ll.name << "':\n  ";
    while (ptr) {
      os << ptr->value << " -> ";
      ptr = ptr->next;
    }
    os << "nullptr\n";
    return os;
  }
};

Look at how we make both the struct and the class templates. We add the template <typename T> directive before both structures. Then, we replaced the explicit int types with T, and inside our class template, we replaced LinkedListNode with a specification of the struct template LinkedListNode<T>.

Also, notice how we mixed the template class definition and the implementation. When using templates, we cannot split things into header/source, everything needs to be together. So we would then implement the other functions right below these definitions.

For example, this is how you would implement addToFront:

template <typename T>
void LinkedList<T>::addToFront(T x) {
  // Create new node.
  LinkedListNode<T>* newNode = new LinkedListNode<T>;
  newNode->value = x;
  newNode->next = head;  // points to old head.

  // Update head/tail pointers.
  if (head == nullptr) {
    head = tail = newNode;
  } else {
    head = newNode;
  }
}

Note that this is also a template! It receives a type for the template, and then implements the method for LinkedList<T>. We would then do the same thing for the other methods (including constructors/destructor) we have in the class.

After implementing all of those, you would end up with a file that contains everything for that template. You can download a complete implementation at the end of this page.

After doing that, we can use the class for different types. For example:

LinkedList<int> myList0("int list");

myList0.addToFront(1);
myList0.addToFront(2);
myList0.addToBack(3);

cout << myList0;

LinkedList<float> myList1("float list");

myList1.addToFront(1.5);
myList1.addToFront(2.345);
myList1.addToBack(3.987);

cout << myList1;

Multiple template arguments

Finally, it’s also possible to have more than one placeholder in your templates. For example, if you want to find the maximum of many different types, you could have each parameter have a different placeholder:

template <typename A, typename B, typename C, typename D>
A findMax(B x, C y, D z) {
  if (x > y) {
    if (x > z) {
      return x;
    }
    return z;
  }
  if (y > z) {
    return y;
  }
  return z;
}

By creating a template like this, with 4 placeholders, the user would need to specify all of them when calling the function. For example, a valid call would be:

findMax<double, float, int, short>(3.4, 2, -1);

Where A = double, B = float, C = int, and D = short.

Complete examples

templatedFunctions.h

#ifndef __TEMPLATEDFUNCTION_H__
#define __TEMPLATEDFUNCTION_H__

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

template <typename A, typename B, typename C, typename D>
A findMax(B x, C y, D z) {
  if (x > y) {
    if (x > z) {
      return x;
    }
    return z;
  }
  if (y > z) {
    return y;
  }
  return z;
}

#endif  // __TEMPLATEDFUNCTION_H__

templatedLinkedList.h

#ifndef __LINKEDLIST_H__
#define __LINKEDLIST_H__

#include <iostream>
#include <string>

namespace ICS45C {
namespace Structures {

template <typename T>
struct LinkedListNode {
  T value;
  struct LinkedListNode* next;
};

template <typename T>
class LinkedList {
 private:
  const std::string name;
  LinkedListNode<T>* head;
  LinkedListNode<T>* tail;

 public:
  LinkedList();
  LinkedList(std::string name);
  ~LinkedList();
  void addToFront(T x);
  void addToBack(T x);
  void printList() const;
  friend std::ostream& operator<<(std::ostream& os, const LinkedList& ll) {
    LinkedListNode<T>* ptr = ll.head;
    os << "Values inside '" << ll.name << "':\n  ";
    while (ptr) {
      os << ptr->value << " -> ";
      ptr = ptr->next;
    }
    os << "nullptr\n";
    return os;
  }
};

template <typename T>
LinkedList<T>::LinkedList() : LinkedList("ICS45C LinkedList") {}

template <typename T>
LinkedList<T>::LinkedList(std::string name)
    : name(name), head(nullptr), tail(nullptr) {}

template <typename T>
LinkedList<T>::~LinkedList() {
  while (head) {
    tail = head->next;
    delete head;
    head = tail;
  }
  head = tail = nullptr;
}

template <typename T>
void LinkedList<T>::addToFront(T x) {
  // Create new node.
  LinkedListNode<T>* newNode = new LinkedListNode<T>;
  newNode->value = x;
  newNode->next = head;  // points to old head.

  // Update head/tail pointers.
  if (head == nullptr) {
    head = tail = newNode;
  } else {
    head = newNode;
  }
}

template <typename T>
void LinkedList<T>::addToBack(T x) {
  // Create new node.
  LinkedListNode<T>* newNode = new LinkedListNode<T>;
  newNode->value = x;
  newNode->next = nullptr;  // points to null since it's new tail.

  // Update head/tail pointers.
  if (tail == nullptr) {
    head = tail = newNode;
  } else {
    tail->next = newNode;
    tail = newNode;
  }
}

template <typename T>
void LinkedList<T>::printList() const {
  std::cout << *this;
}

}  // namespace Structures
}  // namespace ICS45C

#endif  // __LINKEDLIST_H__

test-templates-main.cpp

#include <iostream>

#include "templatedFunctions.h"
#include "templatedLinkedList.h"

using ICS45C::Structures::LinkedList;
using std::cout;

int main() {
  // 1. Using templated functions:
  // 1.1 Single template argument:
  cout << "findMax<int>(1, 4): " << findMax<int>(1, 4) << '\n';
  cout << "findMax<float>(1.2, 1.5): " << findMax<float>(1.2, 1.5) << '\n';

  // 1.2 Multiple template arguments:
  cout << "findMax<double, float, int, short>(3.4, 2, -1): "
       << findMax<double, float, int, short>(3.4, 2, -1) << '\n';

  // 2. Using templated classes:
  LinkedList<int> myList0("int list");

  myList0.addToFront(1);
  myList0.addToFront(2);
  myList0.addToBack(3);

  cout << myList0;

  LinkedList<float> myList1("float list");

  myList1.addToFront(1.5);
  myList1.addToFront(2.345);
  myList1.addToBack(3.987);

  cout << myList1;

  return 0;
}

Further reading

There’s some things about templates that we’re not going to cover in this course. If you want to learn about some of them:

References