In lesson 10.15 -- Function templates, we introduced the challenge of having to create a separate (overloaded) function for each different set of types we want to work with:
#include <iostream>
// function to calculate the greater of two int values
int max(int x, int y)
{
return (x < y) ? y : x;
}
// almost identical function to calculate the greater of two double values
// the only difference is the type information
double max(double x, double y)
{
return (x < y) ? y : x;
}
int main()
{
std::cout << max(5, 6); // calls max(int, int)
std::cout << '\n';
std::cout << max(1.2, 3.4); // calls max(double, double)
return 0;
}
The solution to this was to create a function template that the compiler can use to instantiate normal functions for whichever set of types we need:
#include <iostream>
// a single function template for max
template <typename T>
T max(T x, T y)
{
return (x < y) ? y : x;
}
int main()
{
std::cout << max(5, 6); // instantiates and calls max<int>(int, int)
std::cout << '\n';
std::cout << max(1.2, 3.4); // instantiates and calls max<double>(double, double)
return 0;
}
Related content
We cover how function template instantiation works in lesson 10.16 -- Function template instantiation.
Aggregate types have similar challenges
We run into similar challenges with aggregate types (both structs/classes/unions and arrays).
For example, let’s say we’re writing a program where we need to work with pairs of int
values, and need to determine which of the two numbers is larger. We might write a program like this:
#include <iostream>
struct Pair
{
int first{};
int second{};
};
constexpr int max(Pair p) // pass by value because Pair is small
{
return (p.first < p.second ? p.second : p.first);
}
int main()
{
Pair p1{ 5, 6 };
std::cout << max(p1) << " is larger\n";
return 0;
}
Later, we discover that we also need pairs of double
values. So we update our program to the following:
#include <iostream>
struct Pair
{
int first{};
int second{};
};
struct Pair // compile error: erroneous redefinition of Pair
{
double first{};
double second{};
};
constexpr int max(Pair p)
{
return (p.first < p.second ? p.second : p.first);
}
constexpr double max(Pair p) // compile error: overloaded function differs only by return type
{
return (p.first < p.second ? p.second : p.first);
}
int main()
{
Pair p1{ 5, 6 };
std::cout << max(p1) << " is larger\n";
Pair p2{ 1.2, 3.4 };
std::cout << max(p2) << " is larger\n";
return 0;
}
Unfortunately, this program won’t compile, and has a number of problems that need to be addressed.
First, unlike functions, type definitions can’t be overloaded. The compiler will treat double second definition of Pair
as an erroneous redeclaration of the first definition of Pair
. Second, although functions can be overloaded, our max(Pair)
functions only differ by return type, and overloaded functions can’t be differentiated solely by return type. Third, there is a lot of redundancy here. Each Pair
struct is identical (except for the data type) and same with our max(Pair)
functions (except for the return type).
We could solve the first two issues by giving our Pair
structs different names (e.g. PairInt
and PairDouble
). But then we both have to remember our naming scheme, and essentially clone a bunch of code for each additional pair type we want, which doesn’t solve the redundancy problem.
Fortunately, we can do better.
Author’s note
Before proceeding, please review lessons 10.15 -- Function templates and 10.16 -- Function template instantiation if you’re hazy on how function templates, template types, or function template instantiation works.
Class templates
Much like a function template is a template definition for instantiating functions, a class template is a template definition for instantiating class types.
A reminder
A “class type” is a struct, class, or union type. Although we’ll be demonstrating “class templates” on structs for simplicity, everything here applies equally well to classes.
As a reminder, here’s our int
pair struct definition:
struct Pair
{
int first{};
int second{};
};
Let’s rewrite our pair class as a class template:
#include <iostream>
template <typename T>
struct Pair
{
T first{};
T second{};
};
int main()
{
Pair<int> p1{ 5, 6 }; // instantiates Pair<int> and creates object p1
std::cout << p1.first << ' ' << p1.second << '\n';
Pair<double> p2{ 1.2, 3.4 }; // instantiates Pair<double> and creates object p2
std::cout << p2.first << ' ' << p2.second << '\n';
Pair<double> p3{ 7.8, 9.0 }; // creates object p3 using prior definition for Pair<double>
std::cout << p3.first << ' ' << p3.second << '\n';
return 0;
}
Just like with function templates, we start a class template definition with a template parameter declaration. We begin with the template
keyword. Next, we specify all of the template types that our class template will use inside angled brackets (<>). For each template type that we need, we use the keyword typename
(preferred) or class
(not preferred), followed by the name of the template type (e.g. T
). In this case, since both of our members will be the same type, we only need one template type.
Next, we define our struct like usual, except we can use our template type (T
) wherever we want a templated type that will be replaced with a real type later. That’s it! We’re done with the class template definition.
Inside main, we can instantiate Pair
objects using whatever types we desire. First, we instantiate an object of type Pair<int>
. Because a type definition for Pair<int>
doesn’t exist yet, the compiler uses the class template to instantiate a struct type definition named Pair<int>
, where all occurrences of template type T
are replaced by type int
.
Next, we instantiate an object of type Pair<double>
, which instantiates a struct type definition named Pair<double>
where T
is replaced by double
. For p3
, Pair<double>
has already been instantiated, so the compiler will use the prior type definition.
Here’s the same example as above, showing what the compiler actually compiles after all template instantiations are done:
#include <iostream>
// A declaration for our Pair class template
// (we don't need the definition any more since it's not used)
template <typename T>
struct Pair;
// Explicitly define what Pair<int> looks like
template <> // tells the compiler this is a template type with no template parameters
struct Pair<int>
{
int first{};
int second{};
};
// Explicitly define what Pair<double> looks like
template <> // tells the compiler this is a template type with no template parameters
struct Pair<double>
{
double first{};
double second{};
};
int main()
{
Pair<int> p1{ 5, 6 }; // instantiates Pair<int> and creates object p1
std::cout << p1.first << ' ' << p1.second << '\n';
Pair<double> p2{ 1.2, 3.4 }; // instantiates Pair<double> and creates object p2
std::cout << p2.first << ' ' << p2.second << '\n';
Pair<double> p3{ 7.8, 9.0 }; // creates object p3 using prior definition for Pair<double>
std::cout << p3.first << ' ' << p3.second << '\n';
return 0;
}
You can compile this example directly and see that it works as expected!
For advanced readers
The above example makes use of a feature called class template specialization (covered in future lesson 26.4 -- Class template specialization). Knowledge of how this feature works is not required at this point.
Using our class template in a function
Now let’s return to the challenge of making our max()
function work with different types. Because the compiler treats Pair<int>
and Pair<double>
as separate types, we could use overloaded functions that are differentiated by parameter type:
constexpr int max(Pair<int> p)
{
return (p.first < p.second ? p.second : p.first);
}
constexpr double max(Pair<double> p) // okay: overloaded function differentiated by parameter type
{
return (p.first < p.second ? p.second : p.first);
}
While this compiles, it doesn’t solve the redundancy problem. What we really want is a function that can take a pair of any type. In other words, we want a function that takes a parameter of type Pair<T>
, where T is a template type parameter. And that means we need a function template for this job!
Here’s a full example, with max()
being implemented as a function template:
#include <iostream>
template <typename T>
struct Pair
{
T first{};
T second{};
};
template <typename T>
constexpr T max(Pair<T> p)
{
return (p.first < p.second ? p.second : p.first);
}
int main()
{
Pair<int> p1{ 5, 6 };
std::cout << max<int>(p1) << " is larger\n"; // explicit call to max<int>
Pair<double> p2{ 1.2, 3.4 };
std::cout << max(p2) << " is larger\n"; // call to max<double> using template argument deduction (prefer this)
return 0;
}
The max()
function template is pretty straightforward. Because we want to pass in a Pair<T>
, we need the compiler to understand what T
is. Therefore, we need to start our function with a template parameter declaration that defines template type T. We can then use T
as both our return type, and as the template type for Pair<T>
.
When the max()
function is called with a Pair<int>
argument, the compiler will instantiate the function int max<int>(Pair<int>)
from the function template, where template type T
is replaced with int
. The following snippet shows what the compiler actually instantiates in such a case:
template <>
constexpr int max(Pair<int> p)
{
return (p.first < p.second ? p.second : p.first);
}
As with all calls to a function template, we can either be explicit about the template type argument (e.g. max<int>(p1)
) or we can be implicit (e.g. max(p2)
) and let the compiler use template argument deduction to determine what the template type argument should be.
Class templates with template type and non-template type members
Class templates can have some members using a template type and other members using a normal (non-template) type. For example:
template <typename T>
struct Foo
{
T first{}; // first will have whatever type T is replaced with
int second{}; // second will always have type int, regardless of what type T is
};
This works exactly like you’d expect: first
will be whatever the template type T
is, and second
will always be an int
.
Class templates with multiple template types
Class templates can also have multiple template types. For example, if we wanted the two members of our Pair
class to be able to have different types, we can define our Pair
class template with two template types:
#include <iostream>
template <typename T, typename U>
struct Pair
{
T first{};
U second{};
};
template <typename T, typename U>
void print(Pair<T, U> p)
{
std::cout << '[' << p.first << ", " << p.second << ']';
}
int main()
{
Pair<int, double> p1{ 1, 2.3 }; // a pair holding an int and a double
Pair<double, int> p2{ 4.5, 6 }; // a pair holding a double and an int
Pair<int, int> p3{ 7, 8 }; // a pair holding two ints
print(p2);
return 0;
}
To define multiple template types, in our template parameter declaration, we separate each of our desired template types with a comma. In the above example we define two different template types, one named T
, and one named U
. The actual template type arguments for T
and U
can be different (as in the case of p1
and p2
above) or the same (as in the case of p3
).
Making a function template work with more than one class type
Consider the print()
function template from the above example:
template <typename T, typename U>
void print(Pair<T, U> p)
{
std::cout << '[' << p.first << ", " << p.second << ']';
}
Because we’ve explicitly defined the function parameter as a Pair<T, U>
, only arguments of type Pair<T, U>
(or those that can be converted to a Pair<T, U>
) will match. This is ideal if we only want to be able to call our function with a Pair<T, U>
argument.
In some cases, we may write function templates that we want to use with any type that will successfully compile. To do that, we simply use a type template parameter as the function parameter instead.
For example:
#include <iostream>
template <typename T, typename U>
struct Pair
{
T first{};
U second{};
};
struct Point
{
int first{};
int second{};
};
template <typename T>
void print(T p) // type template parameter will match anything
{
std::cout << '[' << p.first << ", " << p.second << ']'; // will only compile if type has first and second members
}
int main()
{
Pair<double, int> p1{ 4.5, 6 };
print(p1); // matches print(Pair<double, int>)
std::cout << '\n';
Point p2 { 7, 8 };
print(p2); // matches print(Point)
std::cout << '\n';
return 0;
}
In the above example, we’ve rewritten print()
so that it has only a single type template parameter (T
), which will match any type. The body of the function will compile successfully for any class type that has a first
and second
member. We demonstrate this by calling print()
with an object of type Pair<double, int>
, and then again with an object of type Point
.
There is one case that can be misleading. Consider the following version of print()
:
template <typename T, typename U>
struct Pair // defines a class type named Pair
{
T first{};
U second{};
};
template <typename Pair> // defines a type template parameter named Pair (shadows Pair class type)
void print(Pair p) // this refers to template parameter Pair, not class type Pair
{
std::cout << '[' << p.first << ", " << p.second << ']';
}
You might expect that this function will only match when called with a Pair
class type argument. But this version of print()
is functionally identically to the prior version where the template parameter was named T
, and will match with any type. The issue here is that when we define Pair
as a type template parameter, it shadows other uses of the name Pair
within the global scope. So within the function template, Pair
refers to the template parameter Pair
, not the class type Pair
. And since a type template parameter will match to any type, this Pair
matches to any argument type, not just those of class type Pair
!
This is a good reason to stick to simple template parameter names, such a T
, U
, N
, as they are less likely to shadow a class type name.
std::pair
Because working with pairs of data is common, the C++ standard library contains a class template named std::pair
(in the <utility>
header) that is defined identically to the Pair
class template with multiple template types in the preceding section. In fact, we can swap out the pair
struct we developed for std::pair
:
#include <iostream>
#include <utility>
template <typename T, typename U>
void print(std::pair<T, U> p)
{
std::cout << '[' << p.first << ", " << p.second << ']';
}
int main()
{
std::pair<int, double> p1{ 1, 2.3 }; // a pair holding an int and a double
std::pair<double, int> p2{ 4.5, 6 }; // a pair holding a double and an int
std::pair<int, int> p3{ 7, 8 }; // a pair holding two ints
print(p2);
return 0;
}
We developed our own Pair
class in this lesson to show how things work, but in real code, you should favor std::pair
over writing your own.
Using class templates in multiple files
Just like function templates, class templates are typically defined in header files so they can be included into any code file that needs them. Both template definitions and type definitions are exempt from the one-definition rule, so this won’t cause problems:
pair.h:
#ifndef PAIR_H
#define PAIR_H
template <typename T>
struct Pair
{
T first{};
T second{};
};
template <typename T>
constexpr T max(Pair<T> p)
{
return (p.first < p.second ? p.second : p.first);
}
#endif
foo.cpp:
#include "pair.h"
#include <iostream>
void foo()
{
Pair<int> p1{ 1, 2 };
std::cout << max(p1) << " is larger\n";
}
main.cpp:
#include "pair.h"
#include <iostream>
void foo(); // forward declaration for function foo()
int main()
{
Pair<double> p2 { 3.4, 5.6 };
std::cout << max(p2) << " is larger\n";
foo();
return 0;
}