Function Templates & Type Deduction
Learn how to write functions that work with any type. Understand template argument deduction, non-type parameters, and why templates live in headers.
Why Function Templates?
Without templates, writing a function that works for multiple types means duplicating logic. You would write int max(int, int), double max(double, double), std::string max(std::string, std::string) — identical logic repeated for every type. Function templates let you write the algorithm once and let the compiler generate type-specific versions automatically.
A function template is not a function — it is a blueprint from which the compiler instantiates actual functions when you use them with specific types. This happens entirely at compile time, producing code identical to what you would write by hand — zero overhead.
Template Syntax
A function template is introduced with the template keyword followed by a parameter list in angle brackets. You can use typename or class interchangeably for type parameters — they mean exactly the same thing. Modern convention prefers typename.
#include <iostream>
#include <string>
// 'typename' and 'class' are interchangeable here
template <typename T>
T maximum(T a, T b) {
return (a > b) ? a : b;
}
// Equivalent — 'class' means the same thing
template <class T>
T minimum(T a, T b) {
return (a < b) ? a : b;
}
int main() {
std::cout << maximum(3, 7) << '\n'; // deduces T = int
std::cout << maximum(3.14, 2.71) << '\n'; // deduces T = double
std::cout << maximum<std::string>("hello", "world") << '\n';
// explicit instantiation needed — "hello" is const char*, not std::string
}Template Argument Deduction Rules
The compiler deduces template arguments by matching the types of the function arguments against the parameter patterns. The key rules are:
1. Exact match preferred — maximum(3, 7) deduces T = int because both arguments are int.
2. No implicit conversions during deduction — maximum(3, 7.0) is an error: the compiler sees T = int from the first argument and T = double from the second, and refuses to pick one.
3. Array-to-pointer decay — when T appears as a value parameter, arrays decay to pointers. Use T(&)[N] to capture the array type and size.
4. Top-level const/volatile is stripped from value parameters but preserved for reference/pointer parameters.
5. Reference collapsing — if T is deduced as a reference type (in forwarding references), reference collapsing rules apply: T& & becomes T&, T& && becomes T&, T&& & becomes T&, T&& && becomes T&&.
When deduction fails or is ambiguous, you must provide template arguments explicitly: maximum.
Non-Type Template Parameters
Template parameters are not limited to types. Non-type template parameters (NTTPs) accept compile-time values such as integers, pointers, or (since C++20) floating-point and class types. They are powerful for embedding compile-time constants into the type system.
#include <iostream>
#include <array>
#include <cstddef>
// Non-type parameter: N is a compile-time size_t value
template <typename T, std::size_t N>
T sum(const std::array<T, N>& arr) {
T total{};
for (const auto& elem : arr) {
total += elem;
}
return total;
}
// C++20: auto non-type parameter accepts any structural type
template <auto Value>
constexpr auto doubled = Value * 2;
int main() {
std::array<int, 4> nums = {1, 2, 3, 4};
std::cout << sum(nums) << '\n'; // 10, deduces T=int, N=4
std::cout << doubled<21> << '\n'; // 42
std::cout << doubled<3.14> << '\n'; // 6.28 (C++20)
}Every unique combination of template arguments causes the compiler to generate a separate instantiation — a distinct copy of the function in your binary. Calling maximum, maximum, and maximum produces three functions. This is usually fine, but in large codebases templates can contribute to code bloat (larger binaries, longer compile times). Mitigation strategies include: using extern template declarations to control where instantiations happen, factoring type-independent code out of templates, and using type erasure when runtime polymorphism is acceptable.
Because the compiler needs to see the full template definition to instantiate it, templates cannot be split into .h / .cpp files the way regular functions can. If you put a template definition in a .cpp file and try to use it from another translation unit, you will get a linker error (undefined reference). The standard solution is to put the entire template definition in the header file. If compile times become a problem, you can use explicit instantiation: put the definition in a .cpp file and add template int maximum to explicitly instantiate the specializations you need.
Explicit Instantiation
You can explicitly control where and when templates are instantiated. This can reduce compile times in large projects by preventing redundant instantiations across translation units.
// ---- maximum.h ----
template <typename T>
T maximum(T a, T b) {
return (a > b) ? a : b;
}
// Suppress implicit instantiation in every TU that includes this header
extern template int maximum<int>(int, int);
extern template double maximum<double>(double, double);
// ---- maximum.cpp ----
#include "maximum.h"
// Explicit instantiation: generate code here, once
template int maximum<int>(int, int);
template double maximum<double>(double, double);- Function templates are blueprints — the compiler generates concrete functions for each set of template arguments
typenameandclassare interchangeable in template parameter lists- Template argument deduction does not perform implicit conversions — ambiguous deductions are errors
- Non-type template parameters embed compile-time values into the type system
- Templates must be defined in headers (or use explicit instantiation) because the compiler needs the full definition to instantiate
Quiz — Test Your Knowledge
(15 XP)1. What happens when you call `maximum(3, 7.0)` with a template `template<typename T> T maximum(T a, T b)`?
2. Why must template definitions typically be placed in header files?
3. What is the purpose of `extern template int maximum<int>(int, int);`?