Class Templates

Learn how templates can be used to create multiple classes from a single blueprint
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Illustration representing computer hardware
Ryan McCombe
Ryan McCombe
Updated

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.

Why do we need Templates?

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 had no idea what user-defined types we were going to add to our project, but thanks to templates, their code supports those types anyway:

#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

What Are Templates?

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.

Metaprogramming

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.

Deduplication

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.

Diagram showing the difference between class templates, classes and objects

Declaring Template Parameters

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 receives a single int argument, which we’ve called 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:
  // ...
};

Using Template Parameters

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

Class Template Argument Deduction (CTAD)

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.

Multiple Parameters

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; }

Nested Parameters

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 gets complex or difficult to understand, we can alias it to something simpler and more descriptive:

using HelpfulName = Pair<bool, Pair<int, float>>;

Default Parameters

Just like function parameters can have default values, so too can template parameters. Below, we default both parameters to be int:

#include <string>

class Pair {/*...*/}; 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 our template argument list is empty, we can omit the <> if preferred:

class Pair {/*...*/}; int main() { // Creates a Pair<int, int> Pair A; A.first = 42; A.second = 100; }

Non-Typename Parameters

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.

Template Parameters vs Class Members

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

Using Template Types

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

More Class Template Argument Deduction (CTAD)

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.

Preview: Type Traits and Concepts

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 anything.

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.

Struct Templates

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;
};

Summary

In this lesson, we introduced templates in C++, starting with class templates. The key points to remember are:

  • Templates are like recipes for creating other constructs. For example, a class template can be used to create classes
  • Why class templates such as std::pair are useful, and how they can give us more flexibility than what we could get through a single class
  • How to declare class templates with the template keyword and a list of template parameters.
  • How to use template parameters within class templates to define member types and functions.
  • When instantiating a template, the compiler can sometimes deduce what arguments are required using Class Template Argument Deduction (CTAD).
  • Much like functions, templates can have multiple parameters, default parameters and template arguments can themselves be invocations of other templates.

Was this lesson useful?

Next Lesson

Templates and Header Files

Learn how to separate class templates into declarations and definitions while avoiding common linker errors
Illustration representing computer hardware
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Templates
Next Lesson

Templates and Header Files

Learn how to separate class templates into declarations and definitions while avoiding common linker errors
Illustration representing computer hardware
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved