function and class templates

A template is a blueprint the compiler fills in per type, at compile time — the mechanism behind "generic programming" in C++, and the reason the STL's containers and algorithms work identically across unrelated element types with zero runtime overhead. Each distinct type used with a template causes the compiler to generate ("instantiate") a separate, fully concrete function or class for that type.

template <typename T>
T maxOf(const T &a, const T &b) {
  return a > b ? a : b;
}

maxOf(3, 7);          // instantiates maxOf<int>
maxOf(3.5, 2.1);        // instantiates a separate maxOf<double>
maxOf(std::string("a"), std::string("b"));  // and a separate maxOf<std::string>
            

template <typename T1 = int, typename T2 = double>
class Pair {
  T1 first; T2 second;
public:
  Pair(T1 a, T2 b) : first(a), second(b) {}
  T2 combine() { return first + second; }
};

Pair<> p1(4, 5.1);          // uses the defaults: Pair<int, double>
Pair<float, float> p2(4, 5.1);  // explicit types override the defaults
            
One signature-related subtlety worth knowing: for a function that's a specialization of a function template, the return type is part of what identifies it uniquely. For an ordinary (non-template) function, the return type is not part of the signature — two functions differing only in return type can't be overloaded.

non-type template parameters

A template parameter doesn't have to be a type — it can be a compile-time constant value, baked into each instantiation:

template <int increment>
int addFixed(const int &a) { return a + increment; }

int result = addFixed<6>(10);   // 16 — "6" is baked in at compile time, a separate instantiation per value used
            

template specialization

The generic template handles the general case; a specialization overrides it for one specific type where the generic behavior isn't what you want.

template <typename T>
class Formatter {
public:
  Formatter(T x) { std::cout << x << " is not a character\n"; }
};

template <>                        // empty <> marks a full specialization
class Formatter<char> {
public:
  Formatter(char x) { std::cout << x << " is a character\n"; }
};

Formatter<int>('5');     // uses the generic template
Formatter<char>('x');    // uses the char specialization instead
            
The Unwrap trait below specializes on a template-with-arguments (std::optional<T>) rather than a single concrete type — a common way to write one function that works correctly whether its argument is a plain value or a wrapped one, by delegating "what type is actually underneath this" to a trait the compiler resolves per call: TopNotchNote/cpp/templates_specialization_unwrap.cpp

variadic templates

A template parameter pack accepts any number of arguments, of any types — ... on the left of a name means "pack these into a parameter pack"; on the right, it means "unpack this pack." Recursion (with a non-variadic overload to terminate it) is the classic way to process one at a time.

void print() { std::cout << "(done)\n"; }   // terminates the recursion — the pack ran out

template <typename T, typename ...Rest>
void print(T first, Rest ...rest) {
  std::cout << first << " ";
  print(rest...);                             // peel off one argument, recurse on what's left
}

print(1, 2.5, "three", 'f');   // prints "1 2.5 three f (done)"
            

template <typename T>
T sum(T t) { return t; }                       // base case — one argument left

template <typename T, typename ...Rest>
T sum(T t, Rest ...rest) { return t + sum(rest...); }

sum(1, 2, 3, 4);   // 10 — return type is the type of the first argument
            
C++17's fold expressions replace most of this recursion with a single line for the common case: template <typename ...Args> auto sum(Args ...args) { return (args + ...); } — worth knowing the recursive form above regardless, since it's what you'll see in most existing codebases and books.

SFINAE

Substitution Failure Is Not An Error: when the compiler tries substituting a candidate type into a function template's signature and that substitution doesn't form valid code, the compiler doesn't error out — it just quietly removes that candidate from overload resolution and tries the others. This is the mechanism that lets template libraries offer different implementations for different categories of type (e.g. "has this member function" vs. "doesn't") without the caller ever writing an if/else.
Modern C++ (C++20's concepts, or if constexpr for simpler cases) has mostly replaced hand-written SFINAE tricks for new code — it's worth recognizing the term and the idea, but reaching for concepts first when the option is available produces far more readable compiler errors than a SFINAE failure does.

function templates can't be partially specialized

Class templates support partial specialization (specializing for, say, "any std::complex<T>" while leaving T generic, as above). Function templates don't — the closest equivalent for functions is overloading, which resolves by a different, related-but-distinct set of rules. When a function template needs type-category-specific behavior, the idiomatic path is either a full specialization, or an ordinary overload that the compiler prefers when it's a better match.

where to go from here

STL Containers — templates as the mechanism behind every container type.
Lambdas & Functional C++ — templates combined with lambdas for generic callbacks.
constexpr & static — static_assert, often paired with templates for compile-time constraints.
Perfect Forwarding & Universal References — the T&& deduction case that only shows up in a template parameter.

reference

cppreference — templates
cppreference — parameter packs