What you'll learn
Templates let one algorithm or type work across many concrete types without giving up static checking. Concepts make the required operations part of the interface, improving overload selection, diagnostics, and the meaning of generic code.
By the end, you'll be able to:
- Define function and class templates with type and non-type parameters
- Explain deduction, instantiation, overloads, and specialization
- Expand parameter packs with fold expressions
- Constrain generic interfaces with standard and custom concepts
Function templates
A function template is a recipe for a family of functions. When a call supplies arguments, template argument deduction determines types and the compiler instantiates a suitable specialization. The body must be valid for that specialization, not for every imaginable type.
#include <iostream>
#include <string>
template <typename T>
const T& maximum(const T& left, const T& right) {
return left < right ? right : left;
}
int main() {
std::cout << maximum(4, 9) << '\n';
std::string first{"Ada"};
std::string second{"Bjarne"};
std::cout << maximum(first, second) << '\n';
}Watch out
Class and non-type parameters
A class template generates types. Type parameters select contained values or policies; non-type parameters represent compile-time values such as a capacity. Different arguments produce different types, so FixedBuffer<int, 8> andFixedBuffer<int, 16> are not interchangeable.
| Parameter kind | Example | Represents |
|---|---|---|
| Type | typename T | A type chosen by the caller or deduction |
| Non-type | std::size_t N | A compile-time value |
| Template | template<class> class C | Another template |
| Pack | typename... Ts | Zero or more template arguments |
#include <array>
#include <cstddef>
#include <stdexcept>
template <typename T, std::size_t Capacity>
class FixedBuffer {
public:
void push(const T& value) {
if (size_ == Capacity) throw std::length_error{"buffer full"};
data_[size_++] = value;
}
const T& operator[](std::size_t index) const { return data_.at(index); }
std::size_t size() const { return size_; }
private:
std::array<T, Capacity> data_{};
std::size_t size_{};
};Deduction and overload resolution
Deduction uses argument types but does not generally apply conversions to make unrelated template arguments agree. Explicit template arguments can fill what cannot be deduced. Ordinary overload resolution then compares viable function templates and non-template functions.
#include <iostream>
#include <type_traits>
template <typename Left, typename Right>
auto add(Left left, Right right) {
return left + right;
}
int main() {
auto result{add(2, 3.5)};
static_assert(std::is_same_v<decltype(result), double>);
std::cout << result << '\n';
}Note
Variadic templates and folds
A parameter pack captures zero or more types or values. A fold expression combines a pack with an operator. The identity and associativity matter: addition needs an initial zero for an empty pack, while stream insertion naturally folds from the output stream.
#include <iostream>
#include <utility>
template <typename... Values>
auto sum(Values... values) {
return (0 + ... + values);
}
template <typename... Values>
void print_line(Values&&... values) {
(std::cout << ... << std::forward<Values>(values)) << '\n';
}
int main() {
std::cout << sum(3, 4, 5) << '\n';
print_line("items: ", 3, ", total: ", 12);
}Concepts and requires
A concept is a compile-time predicate over template arguments. Standard concepts such asstd::integral, std::ranges::range, andstd::totally_ordered describe reusable semantic categories. Constraints can appear before a declaration, after its parameter list, or directly on an abbreviated parameter.
#include <concepts>
#include <iostream>
template <std::integral T>
T greatest_common_divisor(T left, T right) {
while (right != 0) {
T remainder{left % right};
left = right;
right = remainder;
}
return left;
}
int main() {
std::cout << greatest_common_divisor(84, 30) << '\n';
}Key idea
Custom concepts and organization
A requires-expression checks valid syntax, result types, and nested constraints without executing code. Template definitions usually live in headers because an instantiation site needs to see the full definition. Keep templates small and move non-dependent implementation into ordinary source-file functions when possible.
#include <concepts>
#include <iostream>
#include <string>
template <typename T>
concept Printable = requires(std::ostream& out, const T& value) {
{ out << value } -> std::same_as<std::ostream&>;
};
template <Printable T>
void print(const T& value) {
std::cout << value << '\n';
}
int main() { print(std::string{"constrained"}); }- Constrain at the public boundary, near the template declaration
- Name concepts for semantics: sortable, serializable, clock-like
- Avoid duplicating constraints that the implementation does not need
- Use static_assert inside a type for a local invariant, not as a substitute for an interface constraint
Recap & quick check
Key takeaways
- Templates generate type-safe functions and classes from compile-time arguments.
- Deduction determines template arguments before normal overload selection chooses a candidate.
- Non-type parameters carry compile-time values; packs carry zero or more arguments.
- Concepts state requirements at the interface and improve selection and diagnostics.
- Template definitions generally need to remain visible in headers.
Quick check
1. When is a function-template specialization normally instantiated?
2. What can a non-type template parameter represent?
3. What does typename... Ts declare?
4. What is the best role for a concept?
Next: Module 15 — Standard Containers & Adaptors, where generic contracts meet concrete data structures and complexity guarantees.