Template Methods and Classes
Why Templates?
Without templates you would have bad alternatives with drawbacks for writing code that works with multiple types.
| Alternative | Drawback |
|---|---|
| Re-implement Behavior | Error-prone |
General Code for Common Base Type void* or Object |
Lose type checking |
| Code Generation/Pre-processor | Find-and-replace is not scope- or type-aware |
Function Templates
A function template is a parametrized function that represents a family of functions.
Example
template <typename T>
T maximum(T a, T b) {
return b < a ? a : b;
}
This function returns the maximum of two values, named a and b in the function parameters.
The type of the parameters is a template parameter T.
To use template parameters, use
template</* comma-seprated-list-of-parameters */>
In the example above, the list of parameters is typename T.
The keyword typename introduces a type parameter.
Of the template parameters, the type parameter is the most common but others exist.
You can alternatively use the keyword class to introduce a type parameter but you should use typename.
Types other than class types can be substituted for T.
Usage
To use the template:
#include "maximum.h"
#include <iostream>
#include <string>
int main() {
int i = 42;
std::cout << "maximum(7, i): " << maximum(7, i) << '\n';
double f1 = 3.4; double f2 = -6.7;
std::cout << "maximum(f1, f2): " << maximum(f1, f2) << '\n';
std::string s1 = "mathematics"; std::string s2 = "math";
std::cout << "maximum(s1, s2): " << maximum(s1, s2) << '\n';
}
This outputs
maximum(7,i): 42
maximum(f1,f2): 3.4
maximum(s1,s2): mathematics
Here maximum() is compiled for each of the three types of int, float and std::string.
The first call of maximum int i = 42; maximum(7,i); uses the function template substituting int in place of the template parameter T.
It is semantically equivalent to calling
int maximum(int a, int b) {
return b < a ? a : b;
}
Replacing template parameters with conrete types is called instantiation resulting in an instance of a template.
The other calls to maximum() instantiate the maximum template for double and std::string like they were declared and implemented individually:
double maximum(double, double);
std::string maximum(std::string, std::string);
void Template Type Parameter
void is a valid template type parameter if the code after substitution is valid.
template <typename T>
T foo(T*) {}
The usage
void *vp = nullptr;
foo(vp);
is okay and deduces the function
void foo(void*)
Two-Phase Translation
If a type doesn’t support certain operations in a template, then the instantiation attempt results in a compile-time error.
This will fail since std::complex does not have a comparison operator.
std::complex<float> c1, c2;
maximum(c1, c2);
There are two phases to template compilation:
- At definition time, the template code is checked
- Syntax errors found
- Unknown names are discovered
- Static assertions independent of template parameters are checked
- At instantiation time, the template code is checked for validity
template <typename T>
void foo (T t) {
// Checked at definition time to see if undeclared() known
undeclared();
// Checked at instantiation time to see if undeclared(t) known
undeclared(t);
// Checked at definition time
static_assert(sizeof(int) > 10, "int too small");
// Checked at instantiation time
static_assert(sizeof(T) > 10, "T too small");
}
The checking of names twice is known as two-phase lookup.
Some compilers don’t do all the checks on the first phase though, so buggy template code can compile for templates that are never instantiated.
Compiling and Linking
Normally, a function declaration is enough to use it but not for templates. Instead, the compiler must see the full definition of the template when instantiation is triggered.
Template Argument Deduction
Template parameters are determined by the passed arguments.
FOr maximum(), if two ints are passed to the type parameter T, the compiler deduces that T must be an int.
const and references affect the type too.
template <typename T>
T maximum(T const& a, T const& b) {
return b < a ? a : b;
}
If int is passed, T is deduced as int because the parameters match int const &.
Type Conversions
During type deduction, automatic type conversions are limited.
- Call parameters declared by reference do not apply trivial conversions to type deduction.
- The arguments must declared with the same template parameter
Tmust exactly match
- The arguments must declared with the same template parameter
- Call parameters declared by value only apply trivial conversions that decay
constandvolatilequalifications are ignored- references convert to the referenced type
- raw arrays and functions convert to the corresponding pointer type
- The decayed types must match for two arguments declared with same type parameter
T
Example
Given this declaration of maximum
template <typename T>
T maximum(T a, T b);
// These succeed
int i = 17;
int const c = 42;
maximum(i, c); // ✅: T is deduced as int
maximum(c, c); // ✅: T is deduced as int
int& ir = i;
maximum(i, ir); // ✅: T is deduced as int
int arr[4];
maximum(&i, arr); // ✅: T is deduced as int*
// These are errors
maximum(4, 7.2); // ❌: T can be deduced as int or double
std::string s;
maximum("hello", s); // ❌: T can be deduced as char const* or std::string
To handle these errors:
- Make the arguments match in type with a cast
maximum(static_cast<double>(4), 7.2); // ✅
- Prevent type deduction by explicitly specifying or qualifying the type of
T
maximum<double>(4, 7.2); // ✅
- Specify the parameters may have different types.
Type Deduction for Default Arguments
For default call arguments, type deduction does not work.
template <typename T>
void f(T="");
f(1); // ✅: deduced T to be int so calls f<int>(1)
f(); // ❌: T cannot be deduced
For this to work, need a default argument for the template parameter.
template <typename T=std::string>
void f(T="");
f(); // ✅: T is a std::string by default
Multiple Template Parameters
Template functions have two sets of parameters:
- Template parameters declared in angle brackets
template <typename T> // The template parameter is the type paramater T
- Call parameters declared in parentheses after the function name
T maximum(T a, T b) // a and b are call parameters
There is no limit to the number of template parameters you can have.
If you wanted to allow different types for a and b
template <typename T1, typename T2>
T1 max(T1 a, T2 b) {
return b < a ? a : b;
}
...
auto m = maximum(4, 7.2) // ✅ (but return type is type of first argument)
If using one of the parameter types as the return type, the argument might get converted. In the example above, m has value 7.
To deal with this:
- Use a third template parameter for the return type, or
- have the return type be found by the compiler, or
- declare a “common type” of the two parameter types to be the return type
Return Type Template Parameter
If template and call parameters are not related, or when template parameters cannot be deduced, the template arguments must be specified explicitly as in maximum<double>(4, 7.2).
We can use this to explicitly specify the return type of a template function by adding another template parameter.
template <typename T1, typename T2, typename RT>
RT maximum(T1 a, T2 b);
RT cannot be deduced since return types are not accounted for when argument deduction is performed, and the return type does not appear in the parameters of a function call. So you must specify the template arguments in usage.
maximum<int, double, double>(4, 7.2);
You can re-order the template arguments so that the return type is specified but the input types are deduced.
template <typename RT, typename T1, typename T2>
RT maximum(T1 a, T2 b);
maximum<double>(4, 7.2);
This is slightly better than the previous version but would have also been possible with the one-parameter version. With T maximum(T, T), explicitly calling maximum<double> would have substituted the input types and return type T as double.
Deducing the Return Type
Best approach if the return type depends on the template parameters is to have the compiler do it. This can be done with auto.
template <typename T1, T2>
auto max(T1 a, T2 b) {
return b < a ? a : b;
}
For this to work, return type deduction must be possible from the function body so (1) the code must be available, and (2) the return statements have to match in type.
Prior to C++14, you would need to add in the trailing return type but we won’t cover that fully. The short story is it would need to be defined as
#include <type_traits>
template <typename T1, typename T2>
auto max(T1 a, T2 b) -> typename std::decay<decltype(true?a:b)>::type {
return b < a ? a : b;
}
Gross.
std::decay<> is used which returns the resulting type as a member type.
Because that member type std::decay<>::type is a type itself, it is qualified with typename in the trailing return type expression.
With C++14 the short hand is to use the *_t variants of the type traits reducing that example to
#include <type_traits>
template <typename T1, typename T2>
auto maximum(T1 a, T2 b) -> std::decay_t<decltype(true?a:b)> {
return b < a ? a : b;
}
Here, we replace typename std::decay<...>::type with std::decay_t<...> which is much nicer!
In this case, we can just omit the trailing return type entirely though.
The next example will show a case where the shorthand type traits come in handy.
Without the trailing return type, initialization of type auto always decays.
This is true in general outside of function declarations.
int i = 42;
int const& ir = i;
auto a = ir; // a is of type int
Common Type as Return Type
We can use the “common type” type trait which finds the general type between two (or more) different types.
#include <type_traits>
template<typename T1, typename T2>
std::common_type_t<T1, T2> max(T1 a, T2 b) {
return b < a ? a : b;
}
Here, we explicitly defined the type if you’re uncomfortable with auto types.
Note, std::common_type is a type trait which are defined in <type_traits>.
These are a struct with a member type containing the resulting type and are used like
typename std::common_type<T1, T2>::type
As we said before, in C++14 we can write this more succinctly as std::common_type_t<T1, T2> letting us skip writing (1) typename and (2) ::type in exchange for suffixing _t_.
It’s a pretty nifty implementation that we can’t cover but we get the desired behaviour of maximum(4, 7.2) == maximum(7.2, 4) == 7.2.
For completeness, we note that std::common_type<> also decays as we saw in the last section.
Default Template Arguments
Option 1: Use the Termnary Operator
#include <type_traits>
template<typename T1, typename T2,
typename RT = std::decay_t<decltype(true ? T1() : T2())>>
RT maximum(T1 a, T2 b) {
return b < a ? a : b;
}
- We used
std::decay_tto make sure no reference is returned. - The passed types need default constructors.
We could use std::declval but this would make things more complicated.
Option 2: Use std::common_type_t
For the default value, we can use std::common_type_t.
#include <type_traits>
template<typename T1, typename T2,
RT = std::common_type_t<T1, T2>
RT maximum(T1 a, T2 b) {
return b < a ? a : b;
}
Type deduction can now happen with this ordering of template arguments by using the default value for the return type
auto a = maximum(4, 7.2);
or we can specify the return type explicitly
auto b = maximum<double, int, long double>(7.2, 4);
Here, we still have the problem that all three types need to be specified just to specify the return type. We wouldn’t be able to use the methods above if the return type came first. The main problem is we need the return type as the first template parameter, while being able to deduce it from the argument types. Unfortunately, you can’t reference template parameters that come after.
You can still specify a default for the first template argument, it just can’t depend on the succeeding template arguments.
template <typename RT = long, T1, T2>
RT maximum(T1 a, T2 b) {
return b < a ? a : b;
}
This only makes sense for template parameters with a natural default value. In this case though, the return type template parameter really depends on the input type template parameters and we need type traits to accomplish this.
For total simplicity, automatic return type deduction is better.
Overloading Function Templates
Function templates can be overloaded just like normal functions. Overload resolution is already complicated without templates in the mix and get even more complicated when you involve templates.
int maximum(int a, int b) {
return b < a ? a : b;
}
template <typename T>
T maximum(T a, T b) {
return b < a ? a : b;
}
int main() {
maximum(7, 42); // calls the nontemplate for both ints
maximum(7.0, 42.0); // calls the template for both doubles
maximum(4, 72.0); // fails
maximum<>(7, 42); // calls the template when the angle brackets are present
maximum<int>(7, 42); // calls the template when template parameter is supplied
}
Nontemplate and templated versions can coexist. The compiler will prefer a nontemplate over a template instantiation.
The template is selected if the template can generate a better matching function.
Conclusion
Best solution is usually to have the compiler deduce the return type as in
template <typename T1, typename T2>
auto max(T1 a, T2 b) {
return b < a ? a : b;
}
Key Terms
- template parameter
- type parameter
- call parameters
- instantiation
- instance
- definition time
- instantiation time
- decay
- decayed type
- type trait
References
“C++ Templates: The Complete Guide (2nd edition)” Chapter 1: Function Templates
“C++ Templates: The Complete Guide (2nd edition)” Chapter 2: Class Templates