8. Templates¶
What & why¶
You've been using templates since lesson 1 of the beginner track: std::vector<int>, std::map<std::string, int>, std::sort. A template is a blueprint the compiler uses to generate code for whatever types you supply. You get generic code with zero runtime cost — the generated code is as fast as if you'd written it by hand for each type.
Function templates¶
template <typename T>
T largest(const T& a, const T& b) {
return (a < b) ? b : a;
}
largest(3, 7); // T = int
largest(2.5, 1.5); // T = double
largest(std::string{"pear"}, std::string{"apple"}); // T = std::string
The compiler deduces T from the arguments and instantiates a version for each distinct T. The requirement on T is implicit: it must support < and copying.
Deduction pitfalls¶
largest(3, 7.5); // ERROR: T can't be both int and double
largest<double>(3, 7.5); // OK: explicit template argument
Abbreviated templates (C++20)¶
auto parameters make a function a template automatically:
Handy for short helpers; the explicit template <typename T> form is clearer when types must match.
Class templates¶
#include <stdexcept>
#include <vector>
template <typename T>
class Stack {
public:
void push(T value) { items_.push_back(std::move(value)); }
T pop() {
if (items_.empty()) {
throw std::out_of_range("pop on empty stack");
}
T top = std::move(items_.back());
items_.pop_back();
return top;
}
const T& top() const { return items_.back(); }
bool empty() const { return items_.empty(); }
std::size_t size() const { return items_.size(); }
private:
std::vector<T> items_;
};
Stack<int> numbers;
numbers.push(1);
Stack<std::string> words;
words.push("hello");
Class template argument deduction (CTAD)¶
std::vector v{1, 2, 3}; // std::vector<int>
std::pair p{"Ada", 36}; // std::pair<const char*, int>
std::array a{1.0, 2.0}; // std::array<double, 2>
Non-type template parameters¶
Templates can take values, not just types:
template <typename T, std::size_t N>
class RingBuffer {
public:
void push(const T& v) {
data_[head_] = v;
head_ = (head_ + 1) % N;
if (size_ < N) ++size_;
}
std::size_t size() const { return size_; }
private:
std::array<T, N> data_{};
std::size_t head_{0};
std::size_t size_{0};
};
RingBuffer<double, 8> recent;
N is a compile-time constant; RingBuffer<double, 8> and RingBuffer<double, 16> are different types.
Templates live in headers¶
The compiler needs the template's full definition to instantiate it, in every translation unit that uses it. So templates are defined in headers, not .cpp files. Putting a template's body in a .cpp gives you "undefined reference" linker errors.
Specialization¶
Provide a custom version for a specific type:
template <typename T>
std::string describe(const T&) { return "something"; }
template <>
std::string describe<bool>(const bool& b) { return b ? "yes" : "no"; }
For functions, plain overloading is usually simpler than specialization:
Class templates can be partially specialized:
template <typename T>
struct TypeName { static constexpr const char* value = "unknown"; };
template <typename T>
struct TypeName<T*> { static constexpr const char* value = "pointer"; }; // any pointer
if constexpr: compile-time branching¶
Choose code paths based on type properties, and the discarded branch isn't even compiled for that type:
#include <string>
#include <type_traits>
template <typename T>
std::string to_text(const T& value) {
if constexpr (std::is_same_v<T, std::string>) {
return value;
} else if constexpr (std::is_arithmetic_v<T>) {
return std::to_string(value);
} else {
return "<object>";
}
}
<type_traits> offers many queries: std::is_integral_v<T>, std::is_pointer_v<T>, std::is_floating_point_v<T>, std::remove_cvref_t<T>, ...
Constraining templates with concepts (preview)¶
Unconstrained templates produce notoriously long error messages when used with the wrong type. C++20 concepts state requirements up front:
#include <concepts>
template <std::integral T>
T gcd(T a, T b) {
while (b != 0) {
a = std::exchange(b, a % b);
}
return a;
}
gcd(12, 18); // OK
gcd(1.5, 2.5); // clear error: double does not satisfy std::integral
Or with abbreviated syntax: auto gcd(std::integral auto a, std::integral auto b). The advanced track covers concepts in depth.
Type aliases¶
template <typename T>
using Matrix = std::vector<std::vector<T>>;
Matrix<double> m(3, std::vector<double>(3));
using StringMap = std::unordered_map<std::string, std::string>;
Reading template errors¶
Template errors can run to hundreds of lines. Strategy:
- Find the first
error:line. - Look for "required from here" — that's the line in your code that triggered it.
- Look for the unmet requirement: "no match for
operator<", "no member named ...". - Concepts make this dramatically better — prefer constrained templates.
Common mistakes¶
- Defining templates in a
.cpp→ linker errors. - Deduction conflicts —
largest(1, 2.0). - Forgetting
typenamefor dependent types:typename T::value_type x;. - Code bloat — every instantiation is separate machine code. Usually fine; occasionally matters in embedded work.
- Over-generalizing — a template used with exactly one type is just a harder-to-read function.
Exercises¶
- Write
template <typename T> void print_all(const std::vector<T>&), then generalize it to work with any container usingconst auto&. - Complete
Stack<T>withpeek()returningstd::optional<T>and a constructor fromstd::initializer_list<T>. - Write
template <typename T, std::size_t N> constexpr std::size_t array_size(const T (&)[N])and use it on a C array. - Finish
RingBuffer<T, N>with iteration in oldest-to-newest order and astatic_assert(N > 0). - Write
to_textusingif constexprto handle strings, numbers,bool, and any type with a.to_string()member (use arequiresexpression:if constexpr (requires { value.to_string(); })). - Write a
Pair<K, V>class template and use CTAD to create one without spelling out the types (add a deduction guide if needed).
Recap¶
- Templates are compile-time blueprints; the compiler instantiates code per type, with no runtime cost.
- Function templates deduce
Tfrom arguments;autoparameters create abbreviated templates. - Class templates + CTAD:
std::vector v{1, 2, 3}. - Non-type parameters (
std::size_t N) bake values into types. - Templates must be defined in headers.
if constexpr+<type_traits>for type-dependent code; concepts to constrain templates.