In lesson 11.6 -- Function templates, we took a look at function templates:
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).
In lesson 13.13 -- Class templates, we covered class templates, which allow us to use type template parameters for the type of our data members of class types (struct, classes, and unions):
Related content
We discuss deduction guides in lesson 13.14 -- Class template argument deduction (CTAD) and deduction guides.
In this lesson, we’ll combine elements of both function templates and class templates as we take a closer look at class templates that have member functions.
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.
In the following example, we rewrite the above Pair class template, converting it from a struct to
a class:
The above should be pretty straightforward, but there are a few things worth noting.
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 class 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.
Second, we don’t need deduction guides for CTAD to work with non-aggregate classes. A matching constructor provides the compiler with the information it needs to deduce the template parameters from the initializers.
Third, let’s look more closely at the case where we define a member function for a class template outside of the class template definition:
Since this member function definition is separate from the class template definition, we need to resupply a
template parameter declaration (template <typename T>) so the compiler knows what
T is.
Also, when we define a member function outside of the class, we need to qualify the member function name with
the fully templated name of the class template (Pair<T>::isEqual, not
Pair::isEqual).
Injected class names
In a prior lesson, we noted that the name of a constructor must match the name of the class. But in our class
template for Pair<T> above, we named our constructor Pair, not
Pair<T>. Somehow this still works, even though the names don’t match.
Within the scope of a class, the unqualified name of the class is called an injected class name. In a class template, the injected class name serves as shorthand for the fully templated name.
Because Pair is the injected class name of Pair<T>, within the scope of our
Pair<T> class template, any use of Pair will be treated as if we had written
Pair<T> instead. Therefore, although we named the constructor Pair, the compiler
treats it as if we had written Pair<T> instead. The names now match!
This means that we can also define our isEqual() member function like this:
Because this is a definition for a member function of Pair<T>, we’re in the scope of the
Pair<T> class template. Therefore, any use of Pair is shorthand for
Pair<T>!
Key insight
In lesson 13.14 -- Class template argument deduction (CTAD) and deduction guides, we noted that CTAD doesn’t work with function parameters (as it is argument deduction, not parameter deduction). However, using an injected class name as a function parameter is okay, as it is shorthand for the fully templated name, not a use of CTAD.
Where to define member functions for class templates outside the class
With member functions for class templates, the compiler needs to see both the class definition (to ensure that the member function template is declared as part of the class) and the template member function definition (to know how to instantiate the template). Therefore, we typically want to define both a class and its member function templates in the same location.
When a member function template is defined inside the class definition, the template member function definition is part of the class definition, so anywhere the class definition can be seen, the template member function definition can also be seen. This makes things easy (at the cost of cluttering our class definition).
When a member function template is defined outside the class definition, it should generally be defined immediately below the class definition. That way, anywhere the class definition can be seen, the member function template definitions immediately below the class definition will also be seen.
In the typical case where a class is defined in a header file, this means any member function templates defined outside the class should also be defined in the same header file, below the class definition.
Key insight
In lesson 11.7 -- Function template instantiation, we noted that functions implicitly instantiated from templates are implicitly inline. This includes both non-member and member function templates. Therefore, there is no issue including member function templates defined in header files into multiple code files, as the functions instantiated from those templates will be implicitly inline (and the linker will de-duplicate them).
Best practice
Any member function templates defined outside the class definition should be defined just below the class definition (in the same file).