Templates power the whole standard library; reading and writing them is essential for any serious C++ work.
A template is a pattern from which the compiler generates code for each set of arguments you use. template <typename T> T max_of(T a, T b) works for any T supporting <; calling max_of(3, 7) instantiates max_of<int>. There's no runtime cost: each instantiation is ordinary, fully optimised code.
- Function templates usually deduce their arguments from the call. Deduction needs consistent types, so
max_of(2, 2.5)fails (isTint or double?); writemax_of<double>(2, 2.5). - Class templates like
std::vector<T>parameterise a whole type. Since C++17, CTAD (class template argument deduction) deduces the arguments from the constructor:std::vector v{1, 2, 3};andstd::pair p{1, 2.5};. C++20 extends CTAD to aggregates. - Non-type parameters pass compile-time values:
template <typename T, std::size_t N> class Ring. C++20 allows floating-point and simple class types here as well. - Variadic templates take any number of arguments,
template <typename... Ts>, and fold expressions combine them:(xs + ... + 0).
Because the compiler needs the full definition to instantiate it, template code normally lives entirely in headers.
A template parameter written T&& in a deduced context is a forwarding reference, not an rvalue reference. It binds to anything, and std::forward<T>(arg) passes the argument on as an lvalue or rvalue, exactly as it came in. That's how emplace_back and make_unique pass constructor arguments through.
You can specialise a template for particular arguments, template <> struct Hash<MyType> { ... };, to give a custom implementation.
The traditional downside is error messages: a type that lacks an operation fails deep inside the template body. Concepts, next, fix that.
Example
#include <array>
#include <iostream>
#include <string>
#include <utility>
template <typename T>
T max_of(T a, T b) { return a < b ? b : a; } // any T with operator<
template <typename T, std::size_t N> // type + non-type parameter
class Ring {
public:
void push(T v) { data_[head_++ % N] = std::move(v); }
const T& operator[](std::size_t i) const { return data_[i % N]; }
private:
std::array<T, N> data_{};
std::size_t head_ = 0;
};
template <typename... Ts> // variadic template
auto sum(Ts... xs) { return (xs + ... + 0); } // fold expression
int main() {
std::cout << max_of(3, 7) << ' ' << max_of<double>(2, 2.5) << '\n'; // 7 2.5
std::cout << max_of(std::string{"pear"}, std::string{"apple"}) << '\n'; // pear
Ring<int, 3> r;
for (int i = 1; i <= 5; ++i) r.push(i); // overwrites the oldest
std::pair p{1, 2.5}; // CTAD: std::pair<int, double>
std::cout << r[0] << ' ' << sum(1, 2, 3.5) << ' ' << p.second << '\n'; // 4 6.5 2.5
}Remember
- Templates generate code per argument set at compile time — no runtime overhead
- Template definitions go in headers
- In a deduced
template <class T> f(T&& x),T&&is a forwarding reference; pass it on withstd::forward<T>(x)
Watch out for
- Declaring a template in a header and defining it in a .cpp file, then getting undefined-reference link errors.