C++ course menu
C++ Templates: Function Templates, Class Templates and Concepts
Learn C++ templates: writing generic function templates, type deduction, class templates like a generic Stack, multiple and non-type template parameters, template specialization, C++20 concepts with requires, and why templates live in headers.
On this page
You have used templates since the first vector: std::vector<int> and std::vector<std::string> are the same code, generated for different types. Templates let you write that kind of generic code yourself: one function or class that works for many types, with no run-time cost.
Function templates#
Without templates, a largest function needs a copy for every type. A template writes it once:
#include <iostream>
#include <string>
template <typename T>
T largest(const T& a, const T& b) {
return a > b ? a : b;
}
int main() {
std::cout << largest(3, 9) << '\n'; // T = int
std::cout << largest(2.5, 1.5) << '\n'; // T = double
std::cout << largest(std::string("pear"), std::string("apple")) << '\n'; // T = std::string
}9
2.5
peartemplate <typename T>introduces a type parameter namedT. (classinstead oftypenamemeans the same thing here.)- When you call
largest(3, 9), the compiler deducesT = intand generates anintversion. - The code works for any type that supports what the template uses, here
>and copying.
Each type gets its own compiled version, so templates are as fast as hand-written code.
When deduction fails#
Both arguments must deduce the same T:
template <typename T>
T largest(const T& a, const T& b) { return a > b ? a : b; }
int main() {
largest(3, 2.5); // error: T is int from 3 but double from 2.5
}Fix it by naming the type explicitly, largest<double>(3, 2.5), or by converting an argument.
Templates that work on containers#
A template can accept any container, as long as it supports a range-based for:
#include <iostream>
#include <list>
#include <string>
#include <vector>
template <typename Container>
void print_all(const Container& items) {
for (const auto& item : items) {
std::cout << item << ' ';
}
std::cout << '\n';
}
int main() {
print_all(std::vector<int>{1, 2, 3});
print_all(std::list<std::string>{"a", "b"});
print_all(std::string("xyz"));
}1 2 3
a b
x y zClass templates#
A class template is a blueprint for classes. Here is a small generic stack built on a vector:
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
template <typename T>
class Stack {
public:
void push(const T& value) { items_.push_back(value); }
T pop() {
if (items_.empty()) throw std::out_of_range("pop from empty stack");
T top = items_.back();
items_.pop_back();
return top;
}
bool empty() const { return items_.empty(); }
std::size_t size() const { return items_.size(); }
private:
std::vector<T> items_;
};
int main() {
Stack<int> numbers;
numbers.push(1);
numbers.push(2);
std::cout << numbers.pop() << ' ' << numbers.size() << '\n';
Stack<std::string> words;
words.push("hello");
std::cout << words.pop() << '\n';
}2 1
helloFor class templates you usually write the type in angle brackets: Stack<int>. Since C++17 the compiler can sometimes deduce it from the constructor (std::vector v = {1, 2, 3}; deduces std::vector<int>).
Several parameters, and non-type parameters#
Templates can take several type parameters, and also values known at compile time:
#include <iostream>
#include <string>
template <typename K, typename V>
struct Pair {
K key;
V value;
};
template <typename T, int N>
struct FixedBuffer {
T data[N] = {};
int capacity() const { return N; }
};
int main() {
Pair<std::string, int> p{"age", 21};
FixedBuffer<double, 8> buf;
std::cout << p.key << '=' << p.value << ", capacity " << buf.capacity() << '\n';
}age=21, capacity 8std::array<int, 5> uses exactly this: a type parameter and a size parameter.
Specialization#
Sometimes one type needs different behaviour. A specialization provides it:
#include <iostream>
#include <string>
template <typename T>
std::string describe(const T&) {
return "some value";
}
template <>
std::string describe<bool>(const bool& b) {
return b ? "yes" : "no";
}
int main() {
std::cout << describe(42) << ", " << describe(true) << '\n';
}some value, yesFor functions, a plain overload (std::string describe(bool b)) is usually simpler than a specialization.
C++20 concepts: constraining templates#
When a template is used with an unsuitable type, older compilers produce pages of confusing errors. Concepts state the requirements up front, which gives clear errors and documents the intent:
#include <concepts>
#include <iostream>
template <typename T>
requires std::integral<T> || std::floating_point<T>
T average(T a, T b) {
return (a + b) / 2;
}
template <std::integral T> // shorter syntax
bool is_even(T n) {
return n % 2 == 0;
}
int main() {
std::cout << average(3.0, 4.0) << ' ' << average(7, 9) << '\n';
std::cout << std::boolalpha << is_even(10) << '\n';
// is_even(2.5); // error: the constraint std::integral<double> is not satisfied
}3.5 8
trueThe standard library provides many concepts in <concepts>, such as std::integral, std::floating_point, std::totally_ordered and std::invocable, and you can define your own with concept Name = ...;.
Templates live in headers#
The compiler generates template code when it sees a use with a specific type, so it needs the full template definition, not just a declaration. That is why templates are written entirely in header files, unlike ordinary functions, whose definitions go in .cpp files.
Common mistakes#
Practice#
Practice exercise
Write a function template count_matches(const std::vector<T>& items, const T& target) that returns how many elements equal target. Use it with a vector of ints and a vector of strings.
Show solution
#include <iostream>
#include <string>
#include <vector>
template <typename T>
int count_matches(const std::vector<T>& items, const T& target) {
int count = 0;
for (const T& item : items) {
if (item == target) ++count;
}
return count;
}
int main() {
std::vector<int> nums = {3, 7, 3, 3, 9};
std::vector<std::string> words = {"to", "be", "or", "not", "to", "be"};
std::cout << count_matches(nums, 3) << ' '
<< count_matches(words, std::string("be")) << '\n';
}3 2std::string("be") is needed so that T is deduced as std::string from both arguments. std::count in <algorithm> already does this job in real code.
Key takeaway#
Finished this lesson?