Templates & Generics
One blueprint, a separate concrete function or class generated per type.
Advanced
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
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 <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
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
... 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
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.
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.
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.