4.2. Function templates

Function overloads allow programmers to reuse function names. Functions with the same name may or may not accomplish the same task.

What if our functions actually are intended to do exactly the same thing, but merely on different types?

Function overloads allow us to write functions with the same names and different parameter lists, but each function still requires its own function body, even if it’s only to call another function.

These two overloads each add their parameters, only on different types:

int    sum (int a, int b);

double sum (double a, double b);

These two overloads each add their parameters, only on different types:

C++ provides a way to write a single piece of code that can stand in for an entire class of functions that all do exactly the same thing.

In C++, we can define a template for a function. The template defines a function generating recipe using a generic type as a placeholder. Templates are created using the template keyword, followed by zero or more template parameters in angle brackets <>. For example:

template <template-parameters> function-declaration

Note

Template parameters are optional.

It looks strange to have a template with no template parameters, but it is perfectly legal. For example, as we will explore as part of hash tables, the overloads of std::hash<> normally do not include template parameters:

struct point {
  int x;
  int y;
}

namespace std {
  template <>
  struct hash<point>
  {
      // hash function implementation for a point
  };
}

Using templates, our previous sum functions collapse down to:

template <class T>
T sum (T a, T b) {
  return a+b;
}

When identifying template parameter types, it is common to see either typename or class. As we will see later, a class defines a type, so for the purposes of a template, they are the same. Whether you use ‘typename’ or ‘class’ is a matter of preference.

The identifier T is traditional, but any valid variable name could be used. In introductory template tutorials AnyType is not uncommon.

Templates are normally completely specified in header files. Because templates are not either declarations or definitions, it is an error to write a template in a cpp source file and then try to use it in another source file.

4.2.1. Using templated functions

In short, using functions generated by templates is not very different from a non-templated function.

You can explicitly provide the type:

std::cout << sum<int> (10, 20) << '\n';
std::cout << sum<double> (1.0, 1.5) << '\n';

Or let the compiler deduce the type:

std::cout << sum (10, 20) << '\n';
std::cout << sum (1.0, 1.5) << '\n';

Given a template of a single generic type, take care when mixing types when two or more parameters are involved:

 1#include <iostream>
 2
 3template <class T>
 4T sum (const T a, const T b) {
 5  return a+b;
 6}
 7
 8int main () {
 9  std::cout << sum <double> (10,1.5) << '\n';  // OK
10  std::cout << sum <int>    (10,1.5) << '\n';  // Compiles with warning
11  std::cout << sum          (10,1.5) << '\n';  // Compile error
12
13  return 0;
14}

The call to sum <double> implicitly converts the int to double without warning.

The warning for sum <int> happens because we have explicitly declared the function to take type int, but the second argument is a double. The warning says that the copy of 1.5 passed to sum will be truncated to 1, which is a narrowing conversion.

The error is due to the compiler not be able to find a function overload that meets the calling requirements. Even though sum is a template, the compiler will say:

no matching function for call to 'sum'

note: candidate template ignored: deduced conflicting types
for parameter 'T' ('int' vs. 'double')

In the third call to sum, we asked the compiler to deduce the types. Since the template defines a function with a single type for both arguments and the return value, it doesn’t know which to choose. Both int and double are equally valid choices.

The examples on lines 9 and 10 are valid because the compiler does not need to deduce the type, it was explicitly told the type of the function to generate.

4.2.2. Multiple template parameters

A sum function that only adds numbers of the same type is not particularly useful. Templates also allow defining multiple types to be used in a template with each parameter potentially having a different type.

#include <iostream>

template <typename T1, typename T2>
bool are_equal (const T1& a, const T2& b) {
  return (a==b);
}

int main () {
  if (are_equal(10, 10.0)) {
    std::cout << "x and y are equal\n";
  } else {
    std::cout << "x and y are not equal\n";
  }
}

There is no ‘rule’ that says each template parameter can be used only once in the function declaration.

template <typename T1, typename T2>
  T2 foo (const T1& x, const T2& y) {
    T1 tmp_x = x;
    T2 tmp_y = y < 1? 1: y*y;
    while (tmp_y < tmp_x) {
      ++tmp_x;
      tmp_y *= 2;
    }
    T2 result = tmp_y;
    return result;
  }

4.2.3. Non-generic template parameters

Not every template parameter has to be a class or a typename. Any specific type is a valid template parameter.

The following example defines a template that defines a function that multiplies a value of type T by a provided int N. The template parameter int N can be used in the function body just like any other local variable or function parameter.

Non-generic template parameters may be specified as const if the function body will not modify them.

template <class T, int N>
T multiply (const T& val) {
  return val * N;
}

The multiply template must be called using template parameters

const double two_pi = multiply<double,2>(3.14159);

This is exactly what the std::array class template does. As a wrapper around a raw array, when this container is used, the size of the array is a required template parameter:

std::array<int,4> numbers {2, 4, 6, 8};

Note

Templates that include non-generic template parameters can’t use auto type deduction. For example, std::array needs both the type and the array size. Our multiply example needs both the type and the operand N. While it might be possible to deduce the type based on the argument provided, there is no way for the compiler to ‘deduce’ the second operand N.

template <class T, int N>
T multiply (const T& val) {
  return val * N;
}

int main() {
  multiply(3);     // compile error: multiply times what?
}

More to Explore

You have attempted of activities on this page