In the previous section, we introduced the std::pair
type, which allows us to create containers for storing two pieces of data, of any type.
We provide the two data types we will need to store within chevrons, <
and >
:
std::pair<int, float> MyPair;
We can then access the values stored in those slots using class variables called first
and second
:
#include <utility>
#include <iostream>
int main() {
std::pair<int, float> MyPair{42, 9.8f};
std::cout << "First " << MyPair.first
<< "\nSecond " << MyPair.second;
}
First: 42
Second: 9.8
std::pair
is an example of a template and, in this lesson, we’ll see how we can create our own templates to give similar flexibility.
If we wanted to make a container like std::pair
in a world without templates, we would need to define the types of our first
and second
member variables. For example, we could create a container for storing an int
and a float
like this:
class Pair {
public:
int first;
float second;
};
Obviously, this is not especially flexible. If we wanted to create containers where our first
and second
members have a different type, we’d need to create a different class.
Worse, we may not even know what types we need to support. For example, the developers who wrote the code for std::pair
couldn't have known what user-defined types we might create in our projects, but thanks to templates, their code can support those types:
#include <iostream>
struct Player {
std::string Name{"Anna"};
};
int main() {
std::pair<Player, int> MyPair{Player{}, 42};
std::cout << "Name: " << MyPair.first.Name;
}
Name: Anna
There are many different forms of templates, and we’ll cover them all in this chapter. In this lesson, we’ll start with class templates, which std::pair
is an example of. We can think of a class template as being a recipe for creating classes.
Just as a class can be instantiated to create an object, a class template can be instantiated to create a class.
These classes are created at compile time. We provide a class template, and the compiler can use that template to create classes as needed.
Creating templates is sometimes referred to as metaprogramming, which means writing code that writes code. The code we create (a class template) is used to create even more code (classes)
Templates can have parameters, just like a function. Within our template body, those parameters can be used to customise how each class is created on each instantiation of the template.
For example, within the std::pair
class template, the data type the first
and second
class members are each determined by parameters. And, just like a function, we provide arguments to those parameters when we instantiate our template.
Whilst function arguments are provided between a (
and )
, template arguments are provided within angled brackets, <
and >
. Below, we instantiate the std::pair
template, providing int
as the first template argument, and bool
as the second:
std::pair<int, bool>
As we’ve seen, this will cause the compiler to instantiate a class that has a first
member of type int
and a second
member of type bool
.
The compiler is smart enough to reuse previously created classes, where appropriate. For example, if std::pair<int, bool>
appears multiple times in our code, a class will only be created once. On any subsequent encounters of the same template, with the same argument list, the compiler will reuse the class it created previously.
We don’t need a new class for each new object. As we’ve seen before, a class can be used to create many objects, and that includes classes that were created by class templates. In the following example, we use our template three times, but two of those invocations use the same arguments, in the same order:
std::pair<int, bool> MyPairA { 4, true };
std::pair<int, bool> MyPairB { 41, false };
std::pair<float, int> MyPairC { 1.3f, 2 };
After running this code, we will have two classes: a class designed to store an int and a bool, and a class designed to store a float and an int. We will have two objects that are instances of the first class, and one object that is an instance of the second class.
The syntax for declaring a class template is very similar to declaring a class. The main addition is we provide a list or parameters to be used within the template.
We do this using the template
keyword, following by the parameter list between a set of angled brackets <
and >
. As with a function, each parameter will have a type, and name with which we will refer to that parameter within our template.
Below, we define a class template called Pair
. It has a single non-type template parameter of type int
, which we've named SomeInt
:
template <int SomeInt>
class Pair {
public:
// ...
};
With templates, the most common type of parameter we’ll need is the name of some other type. Below, we’ve changed our argument list to receive a typename
, which we’ve called SomeType
:
template <typename SomeType>
class Pair {
public:
// ...
};
For simple templates, it’s very common for the name of the type to simply be called T
:
template <typename T>
class Pair {
public:
// ...
};
Once we’ve established our template parameters, we can then simply use them within our class template. Our parameter T
is a typename
, so we can use it anywhere a typename
would be expected.
Below, we’ve used T
as the type of our first
and second
member variables. Because we only have a single template argument, first
and second
must share the same type. We’ll expand this to multiple parameters later in this section:
template <typename T>
class Pair {
public:
T first;
T second;
};
We can also use this T
typename as the return and parameter types of class functions, for example. The following shows an alternative implementation of Pair
where we keep first
and second
private, forcing the use of getters and setters:
#include <iostream>;
template <typename T>
class Pair {
public:
Pair(T first, T second) :
mFirst(first), mSecond{second} {}
T GetFirst() const { return mFirst; }
void SetFirst(T first) { mFirst = first; }
T GetSecond() const { return mSecond; }
void SetSecond(T second) { mSecond = second; }
private:
T mFirst;
T mSecond;
};
int main() {
Pair<int> MyPair{42, 5};
std::cout << "First: " << MyPair.GetFirst()
<< "\nSecond: " << MyPair.GetSecond();
}
First: 42
Second: 5
The fact that we’re using a template type does not restrict us from using regular types as well. We can mix them as needed. In the following example, second
is always an int
:
#include <iostream>;
template <typename T>
class Pair {
public:
Pair(T first, int second) :
mFirst(first), mSecond{second} {}
T GetFirst() const { return mFirst; }
void SetFirst(T first) { mFirst = first; }
int GetSecond() const { return mSecond; }
void SetSecond(int second) { mSecond = second; }
private:
T mFirst;
int mSecond;
};
int main() {
Pair<float> MyPair{9.8f, 5};
std::cout << "First: " << MyPair.GetFirst()
<< "\nSecond: " << MyPair.GetSecond();
}
First: 9.8
Second: 5
When working with templates, it is not always necessary to explicitly provide the template arguments. For example, when we’re initializing an object, the compiler can sometimes infer from the constructor arguments what the template arguments would be.
The most common scenario where this can be used is when we’re providing initial values for a class. In the following example, we’re using the Pair
template to create a Pair<int>
class, and then immediately instantiating that class to create an object called MyPair
:
class Pair {/*...*/};
int main() {
Pair<int> MyPair{42, 5};
}
Because we’re providing initial values for our class, the compiler can automatically deduce what type of class we need. Our values 42
and 5
are int
, so the compiler can infer we want to create a Pair<int>
. As such, we can remove the template argument:
class Pair {/*...*/};
int main() {
Pair MyPair{42, 5};
}
This is referred to as Class Template Argument Deduction or CTAD. However, just because this is possible, it doesn’t mean it should always be used. It broadly has the same benefits and drawbacks as any other form of automatic type deduction, such as the auto
keyword.
Different developers and teams are likely to have their own rules on when CTAD, and any other form of type deduction should be used.
Use type deduction only if it makes the code clearer to readers who aren't familiar with the project, or if it makes the code safer. Do not use it merely to avoid the inconvenience of writing an explicit type.
As with functions, our templates can accept as many parameters as we need We separate multiple parameters with a comma ,
Below, we expand our Pair
class to accept two type name parameters, which we’ve called T1
and T2
. We then set the data type of first
to be T1
, whilst second
will have a type of T2
. This replicates the behaviour of the std::pair
type:
template <typename T1, typename T2>
class Pair {
public:
T1 first;
T2 second;
};
Similarly, we provide multiple arguments to our template by separating them with a comma. Below, we create a class from our Pair
template that sets T1
to bool
and T2
to int
. We then instantiate that class to create an object called MyPair
:
class Pair {/*...*/};
int main() {
Pair<bool, int> MyPair;
}
Previously, we’ve seen how a function argument can be an invocation of another function:
Add(1, Add(2, 3));
We can do the exact same thing when using template arguments:
Pair<bool, Pair<int, float>>
An invocation like this will create a pair class whose first
type is bool
, and whose second
type is another instantiation of the Pair
template. Specifically, it will be a pair whose first
type is int
, and whose second
type is float
:
class Pair {/*...*/};
int main() {
Pair<bool, Pair<int, float>> MyPair;
MyPair.first = true;
MyPair.second.first = 42;
MyPair.second.second = 9.8;
}
Remember, if a type becomes complex or difficult to understand, we can create an alias for it using a more descriptive name:
using HelpfulName = Pair<bool, Pair<int, float>>;
Just like function parameters can have default values, so too can template parameters. Below, we default both parameters to be int
:
#include <string>
template <typename T1 = int, typename T2 = int>
class Pair {
public:
T1 first;
T2 second;
};
int main() {
// Creates a Pair<int, int>
Pair<> A;
A.first = 42;
A.second = 100;
// Creates a Pair<std::string, int>
Pair<std::string> B;
B.first = "Hello World";
B.second = 100;
}
From C++17, if all template parameters have default arguments and we're not specifying any explicit template arguments, we can omit the <>
if preferred:
class Pair {/*...*/};
int main() {
// Creates a Pair<int, int>
Pair A;
A.first = 42;
A.second = 100;
}
In most practical use cases, our template parameters will be type names, but not always. It’s valid to use any of the other data types as a template parameter, such as integers, booleans and even user-defined types.
Below, we create a class template that uses an int
parameter, which we’ve called SomeInt
:
#include <iostream>
template <int SomeInt>
class Resource {
public:
int Value{SomeInt};
};
int main() {
Resource<42> A;
std::cout << "Value: " << A.Value;
}
Value: 42
Like a function argument, a template argument does not need to be a static value - it can be any expression that results in a value. However, given that templates are evaluated at compile time, the expression we use as a template argument will also be evaluated at compile time.
This means we can use compile time constants as template arguments, but not runtime expressions such as the value returned from a regular function:
#include <iostream>
class Resource {/*...*/};
int GetInt() { return 42; }
int main() {
constexpr int SomeConst{5};
// This is fine
Resource<SomeConst> A;
// This is not
Resource<GetInt()> B;
}
error C2975: 'SomeInt': invalid template argument for 'Resource', expected compile-time constant expression
Later in this chapter, we’ll introduce more scenarios where the constexpr
specifier can be applied, expanding what we can do at compile time to include function calls and custom object creation.
Note that a class template parameter is not the same as a class member. In the previous examples, we’ve been saving it as a class member (which we called Value
) but we don’t need to.
When our class template is instantiated to create a class, everywhere we’ve used the identifier SomeInt
within that template gets replaced with the value provided as the template argument.
In the following example, once the compiler has completed, we have two classes, both based on the Resource
template. In one class, the body of the Log()
function is std::cout << 42
whilst in the other, it’s std::cout << 5
:
#include <iostream>
template <int SomeInt>
class Resource {
public:
void Log() {
std::cout << SomeInt;
}
};
int main() {
Resource<42> A;
A.Log();
std::cout << ", ";
Resource<5> B;
B.Log();
}
42, 5
When we have a class template, we should remember that the name of the template is not, by itself, the name of a type.
Below, we’re trying to create a Resource
class, which has a class member that we want to be an instance of some Pair
class. This won’t work:
class Pair {/*...*/};
class Resource {
public:
Pair SomePair;
};
error C2955: 'Pair': use of class template requires template argument list
This is because Pair
is not a type - it is a class template. We could use Pair<int, float>
here for example, because that is a type.
If we know the exact type of Pair
we are expecting, we can simply provide the template arguments in this way. Below, we update our Resource
to clarify that it’s member will be an instance of the Pair<int, float>
type:
class Pair {/*...*/};
class Resource {
public:
Pair<int, float> SomePair;
};
However, we often won’t know exactly what instance of a template we’re supporting. As a result, when we have classes or functions that work with template types, those will often also need to be templates themselves.
We cover function templates later in the chapter. Below, we update our Resource
class to be a class template, meaning it can be used to create classes for storing any type of Pair
:
#include <iostream>
class Pair {/*...*/};
template <typename T1, typename T2>
class Resource {
public:
Pair<T1, T2> SomePair;
};
int main() {
Resource<float, int> SomeResource{
Pair<float, int>{9.8f, 42}
};
std::cout << "Second: "
<< SomeResource.SomePair.second;
}
Second: 42
The previous program allows us to use a more advanced example of class template argument deduction. We can remove all the template arguments, if we prefer:
class Pair {/*...*/};
class Resource {/*...*/};
int main() {
Resource SomeResource{Pair{9.8, 42}};
}
This works because we’re first trying to initialize a pair with a float
and int
, therefore the compiler can infer that it needs to create the Pair<float, int>
type to support that.
Then, we’re trying to create a resource type using an instance of Pair<float, int>
, so the compiler creates the Resource<float, int>
class.
Finally, we instantiate the Resource<float, int>
class to create the SomeResource
object.
When working with templates, we can often find ourselves losing some of the benefits of a strongly typed language. For example, when writing member functions for the Resource
class template above, we have no idea what type of data will be in SomePair.first
and SomePair.second
.
We could take this further, and simplify the Resource
template’s two parameters to a single typename:
class Pair {/*...*/};
template <typename T>
class Resource {
public:
T SomePair;
};
int main() {
Resource SomeResource{Pair{9.8, 42}};
}
Now, we don't even know that SomePair
is a pair at all - the developer using our class template could instantiate it with any type:
template <typename T>
class Resource {
public:
T SomePair;
};
int main() {
Resource SomeResource{"Hello there"};
}
That’s great for flexibility, but how can we meaningfully interact with an object when that object could have any type at all?
Later in this chapter, we’ll introduce ways our templates can constrain their parameters to only allow types that have specific traits. For example, our Resource
template could require that the type that is instantiated with have first
and second
member variables.
This means our code can be sure that the object it is working with has the exact characteristics we require, without mandating the exact type of the data, or placing other unnecessary constraints upon it.
We introduce how to do this later in the chapter, in our lessons covering type traits and concepts.
In C++, classes and templates are almost identical. The only difference is that by default, class
members are private whilst struct members are public
. As such, everything we covered in this lesson also works with structs:
template <typename T1, typename T2>
struct Pair {
T1 first;
T2 second;
};
In this lesson, we introduced templates in C++, starting with class templates. The key points to remember are:
std::pair
are useful, and how they can give us more flexibility than what we could get through a single classtemplate
keyword and a list of template parameters.Learn how templates can be used to create multiple classes from a single blueprint
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.