Lesson 2 of 8
Lesson 12.1 introduced the syntax. This lesson is about the part that surprises people: what
exactly T becomes.
The syntax
template <typename T>
T maximum(T a, T b) {
return (a > b) ? a : b;
}
template <typename T> introduces one type parameter named T. Two spellings exist:
template <typename T> // preferred
template <class T> // identical meaning, older style
They mean exactly the same thing. class does not mean T must be a class — maximum<int>
is fine. Prefer typename; it says what it means.
T is just a name. template <typename Element> is often clearer in a container.
Deduction
You almost never write the type. The compiler works it out from the arguments:
maximum(3, 7); // T = int
maximum(2.5, 1.5); // T = double
maximum('a', 'z'); // T = char
That is template argument deduction. It looks at the argument types and solves for T.
Explicit arguments
When deduction cannot decide, or decides wrongly, say what you mean:
maximum<double>(3, 7); // T = double; the int is converted
With T fixed, the arguments are converted to it rather than being used to deduce it.
Mixed types do not deduce
This is the first thing that surprises people:
// WILL NOT COMPILE
#include <iostream>
template <typename T>
T maximum(T a, T b) {
return (a > b) ? a : b;
}
int main() {
std::cout << maximum(3, 7.5) << "\n"; // int from arg 1, double from arg 2
return 0;
}
The compiler says so directly:
error: no matching function for call to 'maximum(int, double)'
note: candidate: 'template<class T> T maximum(T, T)'
note: template argument deduction/substitution failed:
note: deduced conflicting types for parameter 'T' ('int' and 'double')
Read the last line. T cannot be both. Deduction does not apply the usual arithmetic
conversions first — it needs an exact match, so 3 says int and 7.5 says double and there is
no answer.
Three fixes, and which you pick says something different:
maximum<double>(3, 7.5); // fix T explicitly
maximum(3.0, 7.5); // make the call site consistent
or change the template to take two parameters, which is the next section.
Two type parameters
#include <iostream>
#include <string>
#include <vector>
// Two type parameters: the arguments no longer have to match.
template <typename A, typename B>
auto add(A a, B b) {
return a + b; // return type deduced from the expression
}
// Deducing from a container's element type.
template <typename T>
void print_all(const std::vector<T>& values) {
for (const T& v : values) {
std::cout << v << " ";
}
std::cout << "\n";
}
// A deduced array size, which is a non-type parameter (lesson 12.5).
template <typename T, std::size_t N>
std::size_t length_of(const T (&)[N]) {
return N;
}
int main() {
std::cout << add(3, 7.5) << "\n";
std::cout << add(std::string("count: "), "five") << "\n";
std::vector<int> numbers = {1, 2, 3};
std::vector<std::string> words = {"alpha", "beta"};
print_all(numbers);
print_all(words);
int raw[7] = {};
std::cout << "array length " << length_of(raw) << "\n";
return 0;
}
10.5
count: five
1 2 3
alpha beta
array length 7
Three things in that program are worth naming.
auto as the return type asks the compiler to deduce it from the return expression.
int + double is double, so add(3, 7.5) returns double and prints 10.5.
print_all deduces from a pattern, not from a whole type. The parameter is
const std::vector<T>&, so passing a std::vector<std::string> deduces T = std::string.
This pattern-matching is how most library templates work.
length_of recovers an array's length, which a plain int* parameter loses (lesson 7.5). The
size is part of the array's type, so a template can capture it.
The deduction rules that catch people
For a by-value parameter, deduction drops top-level const and references:
#include <iostream>
#include <type_traits>
template <typename T>
void by_value(T) {
std::cout << "T is const: " << std::is_const_v<T>
<< ", T is reference: " << std::is_reference_v<T> << "\n";
}
template <typename T>
void by_const_ref(const T&) {
std::cout << "T is const: " << std::is_const_v<T>
<< ", T is reference: " << std::is_reference_v<T> << "\n";
}
int main() {
const int ci = 42;
const int& cr = ci;
std::cout << std::boolalpha;
by_value(ci); // T deduced as int — the const is dropped
by_value(cr); // T deduced as int — the reference is dropped too
by_const_ref(ci); // T deduced as int — const is in the parameter, not T
return 0;
}
T is const: false, T is reference: false
T is const: false, T is reference: false
T is const: false, T is reference: false
All three say false, and that is the point. Passing a const int& to a by-value parameter
deduces T = int, because the function gets its own copy and copies are not const.
For const T&, the const is written in the parameter, so T itself is still plain int.
Pass by value or by const reference?
The same rule as ordinary functions (lesson 6.4):
template <typename T>
T maximum(T a, T b); // copies both arguments
template <typename T>
const T& maximum(const T& a, const T& b); // no copies
For int and double the copy is free. For a std::string or a std::vector it is not — by
value copies the whole contents on every call.
Default to const T& for a template, because you do not know what T will be. The exception is
when you need your own copy anyway, in which case take it by value and move from it (lesson 9.10).
But note the trap in the second signature: returning const T& means returning a reference to one
of the arguments. If the caller passes a temporary, the reference dangles the moment the statement
ends (lesson 8.5). This is a real bug in real code — std::max has exactly this hazard.
Overloading a template
A non-template function wins over a template when both match exactly:
#include <iostream>
#include <cstring>
template <typename T>
T maximum(T a, T b) {
std::cout << "[template] ";
return (a > b) ? a : b;
}
// A plain overload, for the case where the template does the wrong thing.
const char* maximum(const char* a, const char* b) {
std::cout << "[overload] ";
return (std::strcmp(a, b) > 0) ? a : b;
}
int main() {
std::cout << maximum(3, 7) << "\n";
std::cout << maximum("apple", "banana") << "\n";
return 0;
}
[template] 7
[overload] banana
That overload exists because the template would be wrong here. With T = const char*, a > b
compares pointer addresses, not the text — it would return whichever string happens to sit
higher in memory. The overload uses strcmp, which compares the characters.
This is the general shape: write the template for the common case, and add a plain overload for the type it gets wrong.
Try it
Write a template clamp(value, low, high) returning value limited to the range. Test it with
int and double. Then try calling it with mixed types and fix the error two different ways.
Finally, write a version taking const T& and explain when each is preferable.
Checkpoint
template <typename T>andtemplate <class T>are identical; prefertypename.- Deduction solves for
Tfrom the argument types, with no arithmetic conversions first. maximum(3, 7.5)fails becauseTcannot be bothintanddouble.- Fix it by making the call consistent, giving
Texplicitly, or using two type parameters. - By-value deduction drops top-level
constand references. - Templates can deduce from patterns like
const std::vector<T>&andconst T (&)[N]. - Prefer
const T¶meters, since you do not know how expensiveTis to copy. - Returning
const T&can dangle when the caller passes a temporary. - A non-template overload beats the template on an exact match — useful when the template is wrong.
Questions about this lesson
Log in to ask a question.