Templates in C++
Master function templates, class templates, template specialization, SFINAE, and C++20 Concepts.
Templates in C++
Templates let you write a single piece of code that works for many types, with the compiler generating the concrete versions you actually use. The key insight is that templates are a compile-time mechanism — there is no runtime overhead from genericity. The STL is built entirely on templates: std::vector<T>, std::sort, std::unique_ptr are all templates. Understanding them unlocks the ability to write zero-overhead abstractions and is essential for reading any modern C++ library.
Function Templates
The simplest template takes one or more type parameters. The compiler deduces the type from the arguments and generates a concrete function — max<int>, max<double>, etc. — for each unique set of argument types you actually call it with.
#include <iostream>
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b; // works for any T that supports operator>
}
int main() {
std::cout << max(3, 7) << "\n"; // max<int> — compiler generates int version
std::cout << max(3.14, 2.72) << "\n"; // max<double> — compiler generates double version
std::cout << max('a', 'z') << "\n"; // max<char> — compiler generates char version
}
The compiler deduces T from the argument types. You can also specify it explicitly: max<double>(3, 7.5).
Class Templates
A class template parameterizes an entire class, letting you create type-safe generic data structures. This is how std::vector, std::stack, and std::optional are implemented in the standard library. Each instantiation — Stack<int>, Stack<std::string> — is a completely separate class with its own compiled code.
#include <vector>
#include <stdexcept>
template <typename T>
class Stack {
std::vector<T> data_;
public:
void push(const T& value) { data_.push_back(value); }
void push(T&& value) { data_.push_back(std::move(value)); } // move overload
void pop() {
if (data_.empty()) throw std::underflow_error("Stack is empty");
data_.pop_back();
}
const T& top() const {
if (data_.empty()) throw std::underflow_error("Stack is empty");
return data_.back();
}
bool empty() const { return data_.empty(); }
size_t size() const { return data_.size(); }
};
int main() {
Stack<int> ints;
ints.push(1);
ints.push(2);
ints.push(3);
while (!ints.empty()) {
std::cout << ints.top() << " ";
ints.pop();
}
// prints: 3 2 1
}
Non-Type Template Parameters
Template parameters don’t have to be types — they can be compile-time integer values. This is how std::array<T, N> encodes its size as part of the type, ensuring the size is always known at compile time and no heap allocation is needed.
#include <array>
#include <numeric>
template <typename T, std::size_t N>
T sum(const std::array<T, N>& arr) {
// N is known at compile time — no runtime size parameter needed
return std::accumulate(arr.begin(), arr.end(), T{});
}
int main() {
std::array<int, 5> a{1, 2, 3, 4, 5};
std::cout << sum(a) << "\n"; // 15
}
Template Specialization
Sometimes the generic template implementation is wrong or inefficient for a specific type. Full specialization lets you provide a completely different implementation for one particular type. Partial specialization lets you specialize for a family of types (e.g., all pointers).
#include <cstring>
#include <string>
// Primary template — works for most types
template <typename T>
bool equal(T a, T b) { return a == b; }
// Full specialization for const char* — strcmp instead of pointer comparison
template <>
bool equal<const char*>(const char* a, const char* b) {
return std::strcmp(a, b) == 0; // compare string content, not addresses
}
// Partial specialization: specialize for pointers to any type
template <typename T>
bool equal<T*>(T* a, T* b) { return *a == *b; } // dereference and compare values
Class templates support partial specialization; function templates only support full specialization (use overloading for partial behavior).
Type Traits
The <type_traits> header provides compile-time queries about types. These are the building blocks of SFINAE and Concepts — they let you ask “is this type an integer?”, “does this type have a copy constructor?”, “what is this type without its const qualifier?” at compile time.
#include <type_traits>
#include <iostream>
template <typename T>
void describe() {
std::cout << std::boolalpha;
std::cout << "integral: " << std::is_integral_v<T> << "\n";
std::cout << "floating: " << std::is_floating_point_v<T> << "\n";
std::cout << "pointer: " << std::is_pointer_v<T> << "\n";
std::cout << "const: " << std::is_const_v<T> << "\n";
}
int main() { describe<const int*>(); }
Common traits: std::remove_const_t, std::remove_reference_t, std::decay_t, std::common_type_t, std::conditional_t.
SFINAE with std::enable_if
SFINAE (Substitution Failure Is Not An Error) is a rule that says: when template argument substitution fails, the compiler silently discards that overload instead of raising an error. std::enable_if exploits this to conditionally enable or disable template overloads based on type properties. This was the main tool for type-constrained templates before C++20 Concepts.
#include <type_traits>
#include <iostream>
// Only enabled when T is an integral type — substitution fails for float, etc.
template <typename T,
typename = std::enable_if_t<std::is_integral_v<T>>>
void printBits(T value) {
for (int i = sizeof(T) * 8 - 1; i >= 0; --i)
std::cout << ((value >> i) & 1);
std::cout << "\n";
}
int main() {
printBits(42); // OK — T is int, constraint satisfied
// printBits(3.14); // compile error — substitution fails for double
}
SFINAE works but produces notoriously cryptic error messages. Prefer Concepts (below) for all new C++20 code.
Variadic Templates and Fold Expressions
Variadic templates accept any number of type parameters, enabling type-safe functions like std::make_tuple and std::format. Fold expressions (C++17) provide a clean syntax for applying an operator across the entire parameter pack without recursion.
#include <iostream>
// Sum any number of arguments of mixed types
template <typename... Args>
auto sum(Args&&... args) {
return (... + args); // unary left fold: ((a + b) + c) ...
}
// Print all arguments separated by spaces — comma fold applies the expression to each
template <typename... Args>
void print(Args&&... args) {
((std::cout << args << " "), ...); // comma fold
std::cout << "\n";
}
int main() {
std::cout << sum(1, 2.5, 3, 4.0f) << "\n"; // 10.5
print("hello", 42, 3.14, 'x'); // hello 42 3.14 x
}
C++20 Concepts
Concepts are named constraints on template parameters. They solve the main weakness of SFINAE: when a template constraint is violated, Concepts produce a clear, human-readable error message rather than an impenetrable substitution failure wall. They also serve as documentation — a template <Numeric T> parameter tells readers immediately what types are expected.
#include <concepts>
#include <iostream>
#include <string>
// Define a concept: T must support + and be default-constructible
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
T{};
};
// Concept-constrained function — intent is clear, error messages are readable
template <Addable T>
T sum(T a, T b) { return a + b; }
// Concept in a requires clause (alternative syntax)
template <typename T>
requires std::totally_ordered<T>
T clamp(T val, T lo, T hi) {
return val < lo ? lo : val > hi ? hi : val;
}
// Abbreviated function template with concept (C++20 shorthand)
auto add(std::integral auto a, std::integral auto b) {
return a + b;
}
int main() {
std::cout << sum(1, 2) << "\n"; // 3
std::cout << sum(1.0, 2.5) << "\n"; // 3.5
std::cout << sum(std::string("a"), std::string("b")) << "\n"; // ab
// sum(true, false); // concept not satisfied — clear, readable error
}
Standard library concepts live in <concepts>: std::integral, std::floating_point, std::same_as, std::convertible_to, std::invocable, std::ranges::range, and more.
Template Metaprogramming Basics
Before constexpr and Concepts, templates were used for compile-time computation through recursive struct specializations. This technique — Template Metaprogramming (TMP) — is important to recognize in older code, but the modern approach using constexpr functions is cleaner and should be preferred for new code.
// Classic TMP — compute factorial at compile time via recursive specialization
template <int N>
struct Factorial {
static constexpr int value = N * Factorial<N - 1>::value;
};
template <>
struct Factorial<0> {
static constexpr int value = 1; // base case
};
// Modern equivalent — same result, far more readable
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
static_assert(Factorial<5>::value == 120);
static_assert(factorial(5) == 120);
Prefer constexpr functions and Concepts over TMP structs in modern C++. Reserve TMP for situations where you genuinely need to manipulate types rather than values.