Function Templates in C++
WHY THEY EXIST: Templates let you write a function once and have the compiler generate a type-specific version for every type you call it with. Without templates, you would need to duplicate code for each type (or resort to unsafe void*).
Frequently Asked Questions
QWhy must template definitions go in header files?
QWhat is two-phase name lookup?
QCan template functions be virtual?
QWhen should I use auto parameters vs explicit template parameters?
#include <iostream>
#include <format>
#include <string>
#include <vector>
#include <type_traits>1 Basic function template
Templates generate type-safe code for families of types or values.
Use this when the same logic should work across multiple types.
It improves reuse with compile-time checking and optimization.
Define clear template parameters and constrain behavior where needed.
T is deduced from the arguments.
(or the same translation unit) -- the compiler needs the full definition at each instantiation site. Putting the body in a .cpp file causes linker errors.
Watch out: template code must be in headers
template<typename T>
T max_of(T a, T b) {
return (a > b) ? a : b;
}
// HOW TEMPLATE INSTANTIATION WORKS:
// When you call max_of(3, 7), the compiler deduces T=int and generates a
// concrete function — conceptually max_of_int_int — with every occurrence
// of T replaced by int. This is called *instantiation*.
//
// Each unique set of template arguments creates a separate instantiation
// with its own machine code. max_of<int> and max_of<double> are two
// completely independent functions in the final binary.
//
// Instantiation happens entirely at compile time — there is zero overhead
// compared to hand-written per-type functions. You get type-safety and
// code reuse for free.
//
// Because the compiler must see the full template body to stamp out each
// specialization, the definition must be visible at the point of use.
// In practice this means keeping template definitions in header files.
// Putting the body in a .cpp file and including only a declaration causes
// linker errors (the other translation unit cannot instantiate what it
// cannot see).
//
// There are two kinds of instantiation:
// - Implicit instantiation: triggered automatically when you use the
// template (e.g., calling max_of(3, 7)).
// - Explicit instantiation: you force the compiler to generate a
// specific version with a declaration like:
// template int max_of(int, int);
// in a .cpp file. This can reduce compile times in large projects by
// instantiating once and linking everywhere.2 Multiple template parameters
Templates generate type-safe code for families of types or values.
Use this when the same logic should work across multiple types.
It improves reuse with compile-time checking and optimization.
Define clear template parameters and constrain behavior where needed.
silently if types don't match exactly. Use explicit template arguments when in doubt (e.g., max_of<double>(1, 2.5)).
Watch out: template argument deduction can fail
template<typename T, typename U>
auto add(T a, U b) {
return a + b; // Return type deduced (C++14)
}3 Non-type template parameters
Templates generate type-safe code for families of types or values.
Use this when the same logic should work across multiple types.
It improves reuse with compile-time checking and optimization.
Define clear template parameters and constrain behavior where needed.
Values (not types) as template arguments.
template<int N>
constexpr int power_of_two() {
return 1 << N;
}
template<typename T, std::size_t N>
void print_array(const T (&arr)[N]) {
std::cout << "Array[" << N << "]: ";
for (std::size_t i = 0; i < N; ++i) {
std::cout << arr[i] << ' ';
}
std::cout << '\n';
}4 Template specialization
Templates generate type-safe code for families of types or values.
Use this when the same logic should work across multiple types.
It improves reuse with compile-time checking and optimization.
Define clear template parameters and constrain behavior where needed.
Provide a custom implementation for a specific type.
template<typename T>
std::string type_name() {
return "unknown";
}
// Full specializations for common types
template<> std::string type_name<int>() { return "int"; }
template<> std::string type_name<double>() { return "double"; }
template<> std::string type_name<std::string>() { return "std::string"; }
// HOW SPECIALIZATION RESOLUTION WORKS:
// When you write type_name<int>(), the compiler must decide which
// definition to call. It follows a strict priority order:
//
// 1. First, look for a *full (explicit) specialization* that matches the
// template arguments exactly. type_name<int>() has one above, so it
// wins immediately — returning "int" instead of "unknown".
//
// 2. If no full specialization matches, look for a *partial
// specialization*. This applies to class templates only — function
// templates cannot be partially specialized. (You can achieve a
// similar effect for functions by using overloading or if constexpr.)
//
// 3. If no specialization matches at all, use the *primary template*.
// For type_name<float>() — which has no specialization — the primary
// template would be selected, returning "unknown".
//
// This layered resolution is why you can customize type_name<int>()
// without affecting the general template: the specialization is a
// completely separate function that the compiler prefers when the
// arguments match exactly.5 SFINAE and enable_if (pre-C++20 way to constrain)
Templates generate type-safe code for families of types or values.
Use this when the same logic should work across multiple types.
It improves reuse with compile-time checking and optimization.
Define clear template parameters and constrain behavior where needed.
Only enable this function for integral types.
Watch out: only errors in the *immediate context* count as SFINAE errors. The immediate context is the function signature: return type, parameter types, template parameter default arguments, and explicit template arguments. If the substitution succeeds in the signature but an error occurs inside the *function body*, that is a hard compile error — not SFINAE.
template<typename T>
auto is_even(T value) -> std::enable_if_t<std::is_integral_v<T>, bool> {
return value % 2 == 0;
}
// HOW SFINAE WORKS:
// SFINAE = Substitution Failure Is Not An Error.
//
// When the compiler encounters a call like is_even(42), it tries every
// candidate template. For this overload it substitutes T=int into the
// signature, including the return type:
// std::enable_if_t<std::is_integral_v<int>, bool>
// Since is_integral_v<int> is true, enable_if_t<true, bool> yields bool,
// the substitution succeeds, and this overload is viable.
//
// If you called is_even(3.14), the compiler substitutes T=double:
// std::enable_if_t<std::is_integral_v<double>, bool>
// is_integral_v<double> is false, so enable_if_t<false, bool> has no
// member type — the substitution *fails*. Instead of emitting a compile
// error, the compiler silently removes this overload from the candidate
// set. That is SFINAE in action.
//
//
// C++20 concepts replaced most SFINAE usage with clearer, more readable
// syntax. For example, the function above can be rewritten as:
// template<std::integral T>
// bool is_even(T value) { return value % 2 == 0; }
// Concepts produce better error messages and are easier to compose.6 if constexpr: compile-time branching in templates
constexpr enables compile-time evaluation when inputs are constant expressions.
Use this for pure computations or immutable data that can be resolved at compile time.
It shifts work from runtime to compile time and can improve safety/performance.
Mark eligible functions/objects constexpr and keep them valid for constant evaluation.
Different code paths for different types, no runtime cost.
template<typename T>
std::string describe(T value) {
if constexpr (std::is_integral_v<T>) {
return std::format("{} (integer, {} bytes)", value, sizeof(T));
} else if constexpr (std::is_floating_point_v<T>) {
return std::format("{:.4f} (floating, {} bytes)", value, sizeof(T));
} else {
return "unsupported type";
}
}7 Variadic templates with fold expressions (C++17)
Variadic templates accept zero or more template arguments.
Use this for type-safe APIs that operate on an arbitrary number of arguments.
They replace unsafe C-style variadics with compile-time checked code.
Expand parameter packs directly or with fold expressions.
Accept any number of arguments of any types.
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // Unary right fold
}
template<typename... Args>
void print_all(Args&&... args) {
((std::cout << args << ' '), ...); // Comma fold
std::cout << '\n';
}
// HOW FOLD EXPRESSIONS EXPAND:
// A fold expression collapses a parameter pack with a binary operator.
// The compiler expands it at compile time — there is no loop at runtime.
//
// Given args = {1, 2, 3}:
//
// Unary right fold: (args + ...)
// expands to: (1 + (2 + 3))
// — association starts from the right.
//
// Unary left fold: (... + args)
// expands to: ((1 + 2) + 3)
// — association starts from the left.
//
// Binary left fold: (init + ... + args)
// expands to: (((init + 1) + 2) + 3)
// — an initial value is folded in from the left.
//
// Binary right fold: (args + ... + init)
// expands to: (1 + (2 + (3 + init)))
// — an initial value is folded in from the right.
//
// The comma fold used in print_all above — ((std::cout << args << ' '), ...)
// — expands to a sequence of comma-separated expressions:
// (std::cout << a1 << ' '), (std::cout << a2 << ' '), ...
// Each expression is evaluated left-to-right (guaranteed by the comma
// operator), so the arguments print in order.int main() {
// Basic template
std::cout << std::format("max(3, 7) = {}\n", max_of(3, 7));
std::cout << std::format("max(3.14, 2.72) = {}\n", max_of(3.14, 2.72));
std::cout << std::format("max(\"abc\", \"xyz\") = {}\n",
max_of<std::string>("abc", "xyz"));
// Multiple template parameters
std::cout << std::format("add(1, 2.5) = {}\n", add(1, 2.5));
// Non-type parameters
std::cout << std::format("2^10 = {}\n", power_of_two<10>());
int arr[] = {10, 20, 30, 40};
print_array(arr); // N deduced as 4
// Specialization
std::cout << std::format("type_name<int>: {}\n", type_name<int>());
std::cout << std::format("type_name<double>: {}\n", type_name<double>());
// SFINAE
std::cout << std::format("is_even(42) = {}\n", is_even(42));
// is_even(3.14); // Compile error: not integral
// if constexpr
std::cout << describe(42) << '\n';
std::cout << describe(3.14159) << '\n';
// Variadic templates
std::cout << std::format("sum(1,2,3,4,5) = {}\n", sum(1, 2, 3, 4, 5));
print_all("hello", 42, 3.14, 'x');
return 0;
}