C++ · Intermediate
Templates in C++
In short: A C++ template is a recipe from which the compiler generates a function or class for each type it is used with, so one definition of higher(a, b) serves int, double and std::string with full type checking and no run-time cost. Type parameters are deduced from the arguments of a function template and written explicitly for a class template.
One definition, many types
A template is a recipe from which the compiler generates a function or class for each type you use it with. Written once, higher(41, 58) produces an int version and higher(2.75, 2.4) a double version, each compiled with full type checking as if you had typed it out. There is no run-time cost: every generated version is as fast as a hand-written one.
A function template starts with template <typename T>; the keyword class in place of typename means the same thing. Inside, T stands for whatever type the call supplies. The compiler deduces T from the arguments, so they must agree: higher(7, 7.5) fails because T would have to be int and double at once. Naming the type explicitly, higher<double>(7, 7.5), switches deduction off for that parameter and ordinary conversions apply. Deduction never looks at the return type or at how the result is used.
A class template, template <typename T> class Extremes, is used by writing the type: Extremes<int>, Extremes<std::string>. Every member function is generated for that T, and members can have type T. Writing T low_{} value-initialises the member, giving 0 for numbers and an empty std::string, the safe default when the type is unknown. Since C++17, class template argument deduction lets you omit the angle brackets when constructor arguments pin the type down, as in std::pair p(1, 2.5); with no such arguments you must still write them.
Template parameters need not be types. template <typename T, std::size_t N> with a parameter const T (&items)[N] deduces the length of a built-in array from the argument, so the function cannot be handed the wrong size. Also new in C++17, if constexpr chooses between branches at compile time and discards the other, so the discarded branch may contain code that would not compile for the current T.
Because code is generated only when a template is used, the full template must be visible at the point of use: definitions live in header files, not in a separate .cpp. The generated code compiles only if T supports every operation the template performs; Extremes<T> uses <, so a T without it fails at instantiation with a long message naming the missing operator.
Syntax
template <typename T> // function template
T higher(T a, T b) { return (b > a) ? b : a; }
template <typename T> // class template
class Box {
public:
void put(const T& v) { item_ = v; }
T get() const { return item_; }
private:
T item_{}; // value-initialised: 0, empty string, nullptr...
};
template <typename T, std::size_t N> // non-type parameter deduced from an array
std::size_t length(const T (&)[N]) { return N; }
higher(3, 9); // T deduced as int
higher<double>(3, 9.5); // T stated explicitly
Box<std::string> b; // class template: the type is writtenA template can take several parameters, template <typename K, typename V>, and a parameter can have a default, template <typename T = int>.
A function template and type deduction
One higher function used with int, double and std::string, plus one call where the type is stated explicitly so that mixed arguments convert.
#include <iostream>
#include <string>
template <typename T>
T higher(T a, T b) {
return (b > a) ? b : a;
}
template <typename T>
void announce(const std::string& what, T value) {
std::cout << what << ": " << value << "\n";
}
int main() {
announce("top score", higher(41, 58)); // T deduced as int
announce("dearer loaf", higher(2.75, 2.4)); // T deduced as double
announce("later name", higher(std::string("Mira"), std::string("Leo")));
announce("mixed", higher<double>(7, 7.5)); // T given explicitly: 7 converts
return 0;
}Output
top score: 58 dearer loaf: 2.75 later name: Mira mixed: 7.5
Each call deduces T from its arguments, so three separate higher functions are generated and announce is generated for three value types too. The comparison uses >, which std::string supports lexicographically, so Mira beats Leo. The last call would not deduce, because 7 is int and 7.5 is double; writing higher<double> fixes T and lets 7 convert.
A class template that tracks extremes
Extremes<T> remembers the lowest and highest value it has seen. It is used once for temperatures and once for names; the only requirement on T is that < works.
#include <iostream>
#include <string>
template <typename T>
class Extremes {
public:
void add(const T& v) {
if (count_ == 0 || v < low_) low_ = v;
if (count_ == 0 || high_ < v) high_ = v;
++count_;
}
T low() const { return low_; }
T high() const { return high_; }
int count() const { return count_; }
private:
T low_{}; // value-initialised: 0 for int, "" for std::string
T high_{};
int count_ = 0;
};
int main() {
Extremes<int> temps;
int n;
std::cin >> n;
for (int i = 0; i < n; ++i) {
int t;
std::cin >> t;
temps.add(t);
}
std::cout << "temperatures: " << temps.low() << " to " << temps.high()
<< " (" << temps.count() << " readings)\n";
Extremes<std::string> names;
std::string s;
while (std::cin >> s) names.add(s);
std::cout << "names: " << names.low() << " to " << names.high() << "\n";
return 0;
}Input given to the program: 5 ↵ -3 4 12 -7 9 ↵ Ruth Amos Zola Bea
Output
temperatures: -7 to 12 (5 readings) names: Amos to Zola
Extremes<int> and Extremes<std::string> are two distinct classes generated from one definition. low_{} and high_{} start as 0 and as an empty string respectively, and count_ guards the first add so that the initial values are replaced rather than compared. The class deliberately uses only <, written both ways round, so any type with that operator works.
Deducing an array length and choosing a branch at compile time
total works for any array type and never needs the length passed; report prints differently for floating-point types using if constexpr.
#include <cstddef>
#include <iostream>
#include <type_traits>
template <typename T, std::size_t N>
T total(const T (&items)[N]) { // N is deduced from the array's size
T sum{};
for (std::size_t i = 0; i < N; ++i) sum += items[i];
return sum;
}
template <typename T>
void report(const char* label, T value) {
if constexpr (std::is_floating_point_v<T>) { // decided at compile time (C++17)
std::cout << label << " " << value << " (decimal)\n";
} else {
std::cout << label << " " << value << " (whole)\n";
}
}
int main() {
int stops[] = {4, 6, 3, 5};
double fares[] = {1.5, 2.25, 1.75};
report("stops:", total(stops));
report("fares:", total(fares));
return 0;
}Output
stops: 18 (whole) fares: 5.5 (decimal)
total(stops) deduces T as int and N as 4 from the array itself, so the size can never disagree with the data. In report, std::is_floating_point_v<T> (from <type_traits>) is known while compiling, and if constexpr keeps only the matching branch; the other is discarded for that instantiation rather than compiled and skipped. With a plain if both branches would have to compile for every T.
Common mistakes
Calling a template with arguments of different types
Why it goes wrong:
higher(3, 2.5)asks the compiler to deduceTas bothintanddouble; it reports conflicting deductions rather than picking one.Fix: Convert one argument or state the type:
higher<double>(3, 2.5).Defining a template in a .cpp file and using it from another
Why it goes wrong: No code exists until the template is instantiated, and the other file cannot see the definition, so the link fails with an undefined reference.
Fix: Put the whole template, declaration and definition, in the header.
Using an operation the type does not have
Why it goes wrong:
Extremes<MyStruct>compiles only ifMyStructhas<. The error appears at the instantiation, deep inside the template, and can run to many lines.Fix: Read the first line of the error, which names the missing operator, and provide it or choose a type that has it.
C++ · fixbool operator<(const MyStruct& a, const MyStruct& b) { return a.key < b.key; }Omitting the type of a class template with no constructor arguments
Why it goes wrong:
Extremes e;has nothing to deduce from; C++17 deduction works only when constructor arguments determine every parameter.Fix: Write the type:
Extremes<int> e;.
Where you use this
Templates replace copy-and-paste. A matrix class that must work for int, double and long long, a clamp function, a fixed-size ring buffer for sensor samples, a Result<T> that carries either a value or an error: each is one template instead of several near-identical types kept in step by hand. In most programs you consume templates far more than you write them, because std::vector, std::map and std::set and the STL algorithms are all templates.
Write your own when you notice two functions that differ only in a type, and keep the requirements on T small and obvious, as Extremes does with a single <.
template <typename T>
class RingBuffer {
public:
explicit RingBuffer(std::size_t capacity) : items_(capacity) {}
void push(const T& v) { items_[next_++ % items_.size()] = v; }
private:
std::vector<T> items_;
std::size_t next_ = 0;
};Key points
template <typename T>introduces a type parameter; the compiler generates code per type used.- Function template arguments are deduced from the call and must agree; write
f<Type>(...)to state them. - Class templates are used as
Name<Type>; C++17 can deduce the type only from constructor arguments. - Non-type parameters such as
std::size_t Ncan be deduced from array arguments. if constexpr(C++17) selects a branch at compile time and discards the other.- Templates must be fully visible where they are used, so they live in headers.
Try it yourself
smaller is written for int only, so the second call truncates 2.5 and 3.75 to integers and prints 2. Turn smaller into a function template so it works for both calls and the program prints 9 and then 2.5.
#include <iostream>
// make this a template so it works for int and double
int smaller(int a, int b) {
return (b < a) ? b : a;
}
int main() {
int a, b;
double x, y;
std::cin >> a >> b >> x >> y;
std::cout << smaller(a, b) << "\n";
std::cout << smaller(x, y) << "\n";
return 0;
}14 9 ↵ 2.5 3.759
2.5#include <iostream>
template <typename T>
T smaller(T a, T b) {
return (b < a) ? b : a;
}
int main() {
int a, b;
double x, y;
std::cin >> a >> b >> x >> y;
std::cout << smaller(a, b) << "\n";
std::cout << smaller(x, y) << "\n";
return 0;
}Practise this
Exercises for this lesson are in the C++ practice set.
Related lessons
- Functions in C++Defining and calling C++ functions: return types, parameters, prototypes, pass by value versus by reference, default arguments and overloading, with examples.10 min
- Vectors in C++ (std::vector)How std::vector works in C++: creating and growing a vector, indexing with [] and at(), passing vectors to functions, erasing elements and building 2D grids.11 min
- Maps and Sets in C++How std::map and std::set store sorted keys in C++, how [] and find differ, how to count and look up safely, and when unordered_map is the better choice.12 min
Frequently asked questions
Are C++ templates slower than ordinary functions?
No. A template is expanded into an ordinary function or class for each type at compile time, so the generated code is exactly what you would have written by hand and runs at the same speed. The cost is paid in compile time and in longer error messages, not at run time.
Why do template errors only appear when I call the function?
The compiler checks the template's general shape when it reads it, but it can only check that T supports each operation once it knows what T is, which happens at the call. That is why an error about a missing < points at the line inside the template and mentions the call that instantiated it.
How this page was checked. Every program on it was run with GCC 13 (C++17) at build time by the publishing checks, and the output shown is what it printed. Running C++ inside the browser is not available yet, so the Run button is absent rather than pretending; copy the code and run it with GCC 13 (C++17) locally.