As we already discussed 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;
}
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 are 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 definition of
Pair
as an erroneous redeclaration of the first definition ofPair
. - 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 ourmax(Pair)
functions (except for the return type).
We could solve 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:
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 will 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 of 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.
Inside main, we can instantiate Pair
object using whatever type 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 has been instantiated, so the compiler will use the prior type definition.
Class templates with member functions
We took a look at function templates:
template <typename T> // this is the template parameter declaration
T max(T x, T y) // this is the function template definition for max<T>
{
return (x < y) ? y : x;
}
With a function template, we can define type template parameters (e.g. typename T
) and then use them as the type of our function parameters (T x, T y
).
#include <iostream>
template <typename T>
struct Pair
{
T first{};
T second{};
};
// Here's a deduction guide for our Pair (required in C++17 or older)
// Pair objects initialized with arguments of type T and T should deduce to Pair<T>
template <typename T>
Pair(T, T) -> Pair<T>;
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;
}
Type template parameters in member functions
Type template parameters defined as part of a class template parameter declaration can be used both as the type of data members and as the type of member function parameters.
For example:
#include <ios> // for std::boolalpha
#include <iostream>
template <typename T>
class Pair
{
private:
T m_first{};
T m_second{};
public:
// When we define a member function inside the class definition,
// the template parameter declaration belonging to the class applies
Pair(const T& first, const T& second)
: m_first{ first }
, m_second{ second }
{
}
bool isEqual(const Pair<T>& pair);
};
// When we define a member function outside the class definition,
// we need to resupply a template parameter declaration
template <typename T>
bool Pair<T>::isEqual(const Pair<T>& pair)
{
return m_first == pair.m_first && m_second == pair.m_second;
}
int main()
{
Pair p1{ 5, 6 }; // uses CTAD to infer type Pair<int>
std::cout << std::boolalpha << "isEqual(5, 6): " << p1.isEqual( Pair{5, 6} ) << '\n';
std::cout << std::boolalpha << "isEqual(5, 7): " << p1.isEqual( Pair{5, 7} ) << '\n';
return 0;
}
First, because our class has private members, it is not an aggregate, and therefore can't use aggregate initialization. Instead, we have to initialize our class objects using a constructor.
Since our data members are of type T
, we make the parameters of our constructor type const T&
, so the user can supply initialization values of the same type. Because T
might be expensive to copy, it's safer to pass by const reference than by value.
Note that when we define a member function inside the class template definition, we don't need to provide a template parameter declaration for the member function. Such member functions implicitly use the class template parameter declaration.
Let's take look more closely at the case where we define a member function for a class template outside of the class template definition:
template <typename T>
bool Pair<T>::isEqual(const Pair<T>& pair)
{
return m_first == pair.m_first && m_second == pair.m_second;
}
Since this member function definition is separate from this class template definition, we need to resupply a template parameter declaration (template <typename T>
) so the compiler knows what T
is.