Templates — Advanced
A basic function template gives you one implementation that works for many types — advanced templates let you control which implementation gets picked, ask the compiler questions about a type before the program runs, and even compute results entirely at compile time.
A plain function template like template<typename T> T max(T a, T b) already saves you from writing max_int, max_double, and max_string by hand — the compiler stamps out whichever version a call site actually needs. Advanced template techniques build on that same substitution mechanism to do far more: pick a completely different implementation for one specific type (specialization), silently discard an overload that would not compile for a given type instead of erroring out (SFINAE), ask at compile time whether a type has a certain member or supports a certain operation (detection idioms and, since C++20, concepts), accept an arbitrary number of arguments of arbitrary types (variadic templates), and even run genuine computation — loops, branches, arithmetic — before main() ever starts (template metaprogramming). These are the tools that make the STL possible, and they show up constantly in library code, generic containers, and senior-level C++ interviews. This page builds each one up from a concrete problem it solves.
Full Template Specialization
Specialization means telling the compiler: for this exact type, do not generate an implementation from the generic template — use this hand-written one instead. It is useful whenever the generic logic is wrong, inefficient, or simply inapplicable for a particular type.
template<> struct Describe<bool> { ... }; is not a new template — it is a complete, separate definition that the compiler uses only when T is exactly bool. Describe<int> still goes through the generic body; Describe<bool> never does. The compiler picks between them purely by matching the template argument, at compile time, with zero runtime cost.
Function templates can be specialized too, but in modern C++ this is rarely the right tool — an ordinary overload almost always reads better and is chosen the same way. Specialization is used far more often for class templates, especially traits-style ones, which is exactly what the next section builds on.
#include <iostream>
#include <string>
template<typename T>
struct Describe
{
static void print(const T& value)
{
std::cout << "generic value: " << value << "\n";
}
};
// Full specialization for bool: the generic version would print 1 or 0,
// which is technically correct but not very readable.
template<>
struct Describe<bool>
{
static void print(const bool& value)
{
std::cout << "boolean: " << (value ? "true" : "false") << "\n";
}
};
int main()
{
Describe<int>::print(42);
Describe<bool>::print(true);
}Partial Specialization
A full specialization fixes every template parameter to one concrete type. A partial specialization instead matches a pattern — a whole family of types, such as "any pointer" or "any pair where both members are the same type" — while still leaving some part generic. Partial specialization is only available for class templates, not function templates; C++ never added it for functions because overloading already covers most of the same ground for them.
TypeName<T*> is not one specialization but an entire family: the compiler pattern-matches the actual argument against T*, and any pointer type at all — int*, double*, char* — takes this branch instead of the generic one, with T deduced from whatever sits before the *.
When choosing between the generic template, a partial specialization, and a full specialization that all could apply, the compiler always prefers the most specific match: full specialization beats partial specialization, which beats the generic template. This is the same "most specific wins" rule that makes overload resolution predictable.
#include <iostream>
template<typename T>
struct TypeName
{
static void print() { std::cout << "some type\n"; }
};
// Partial specialization: matches T* for ANY T, not one fixed type.
template<typename T>
struct TypeName<T*>
{
static void print() { std::cout << "a pointer type\n"; }
};
int main()
{
TypeName<int>::print(); // generic version
TypeName<int*>::print(); // matches TypeName<T*>, with T = int
TypeName<double*>::print(); // matches TypeName<T*>, with T = double
}SFINAE and std::enable_if
SFINAE stands for "Substitution Failure Is Not An Error." When the compiler tries to substitute a candidate template's parameters into a function signature and that substitution produces something invalid — a type that does not exist, an expression that does not compile — it does not stop with a hard error. It simply removes that candidate from the overload set and keeps looking, as if the candidate had never been written. SFINAE turns that quiet removal into a deliberate tool for choosing between implementations based on a type's properties.
std::enable_if<Condition, T>::type is defined to equal T when Condition is true, and is simply not defined at all when Condition is false.
For describe(42), the first overload's Condition (std::is_integral<int>::value) is true, so ::type resolves to void and that overload compiles normally.
For the same call, the second overload's Condition is false, so std::enable_if<false, void>::type does not exist — substituting T = int into it fails.
That failure happens in the function's signature (the 'immediate context'), which is exactly where SFINAE applies — the compiler discards the second overload silently instead of raising an error.
Only one overload survives for any given T, so the call is unambiguous.
Each overload's return type is only valid for the type category it targets. The one whose condition fails does not merely return something unexpected — it fails to exist as a candidate at all, so the two overloads never conflict.
#include <iostream>
#include <type_traits>
template<typename T>
typename std::enable_if<std::is_integral<T>::value, void>::type
describe(T value)
{
std::cout << value << " is an integer type\n";
}
template<typename T>
typename std::enable_if<std::is_floating_point<T>::value, void>::type
describe(T value)
{
std::cout << value << " is a floating-point type\n";
}
int main()
{
describe(42); // picks the integral overload
describe(3.14); // picks the floating-point overload
}Detection Idioms
SFINAE also answers a more open-ended question: does this type have a member function called size(), or support operator<<, at all? The classic C++11/14 tool for this is std::void_t combined with a partial specialization — a pattern known as the detection idiom.
std::void_t<...> is a template alias that maps any well-formed set of types to void and is ill-formed if any of them is not. For std::vector<int>, decltype(std::declval<T>().size()) is a valid type (size_t), so void_t produces void and the specialization matches, making has_size inherit from true_type. For plain int, .size() does not exist, that decltype is ill-formed, SFINAE quietly discards the specialization, and the primary template (false_type) is used instead. std::declval<T>() conjures a fake T value purely for use in an unevaluated context like decltype, without needing a real object or a constructor call.
#include <iostream>
#include <vector>
#include <type_traits>
template<typename T, typename = void>
struct has_size : std::false_type {};
// This partial specialization only matches when decltype(...) inside
// void_t is well-formed -- i.e., when T actually has a .size() member.
template<typename T>
struct has_size<T, std::void_t<decltype(std::declval<T>().size())>>
: std::true_type {};
int main()
{
std::cout << std::boolalpha;
std::cout << has_size<std::vector<int>>::value << "\n"; // true
std::cout << has_size<int>::value << "\n"; // false
}Concepts and requires (C++20)
SFINAE-based constraints work, but the error messages they produce when a call does not match are notoriously unreadable — a wall of substitution failures with no clear statement of what actually went wrong. C++20 concepts replace most of that with named, readable constraints checked directly by the compiler.
concept Numeric = std::is_arithmetic_v<T>; gives a boolean compile-time condition a name. template<Numeric T> constrains T directly in the template parameter list; requires Numeric<T> spells the same constraint as a clause after the template header. Both are checked before the function body is even considered, and a violated constraint produces a message naming Numeric specifically, instead of a cascade of failed substitutions from a hand-rolled enable_if.
Concepts can also be written inline with a requires expression, listing the exact operations a type must support, without giving the constraint a name first.
The second requires here starts a requires-expression: it declares dummy parameters a and b and lists expressions that must compile for T to satisfy the constraint. This is the modern, more readable replacement for expression SFINAE with decltype and void_t.
#include <iostream>
#include <concepts>
template<typename T>
concept Numeric = std::is_arithmetic_v<T>;
template<Numeric T>
T add(T a, T b)
{
return a + b;
}
// Equivalent, spelled with an explicit requires clause:
template<typename T>
requires Numeric<T>
T multiply(T a, T b)
{
return a * b;
}
int main()
{
std::cout << add(2, 3) << "\n"; // OK: int satisfies Numeric
std::cout << multiply(2.5, 4.0) << "\n"; // OK: double satisfies Numeric
// add(std::string("a"), std::string("b")); // would fail to compile
// with a clear message: "constraints not satisfied", naming Numeric
}template<typename T>
requires requires(T a, T b) { a + b; }
T add_inline(T a, T b)
{
return a + b;
}Variadic Templates and Fold Expressions
A variadic template accepts a parameter pack — an arbitrary number of template arguments of arbitrary types — instead of a fixed parameter list. This is how std::make_unique, std::tuple, and std::printf-style formatting functions work regardless of how many arguments are passed.
Args... is a template parameter pack; args... in the function parameter list is a matching function parameter pack — both can hold any number of entries, deduced from the call.
sizeof...(Args) would report how many types were passed, if the count itself were needed.
(args + ...) is a C++17 fold expression: it expands to args1 + (args2 + (args3 + args4)) for however many arguments were actually passed.
((std::cout << args << " "), ...) folds over the comma operator instead, so each argument gets its own std::cout << ... << " "; statement, executed left to right.
Before C++17, the same effect required recursive template unpacking: a function handling one argument plus the 'rest' of the pack, recursing until the pack was empty.
print_all(1, "two", 3.0, 'x') expands at compile time into four chained insertions, and sum(1, 2, 3, 4) expands into 1 + 2 + 3 + 4 — the compiler generates exactly the code you would have written by hand for that specific call, with no runtime loop or overhead for the variable argument count.
#include <iostream>
template<typename... Args>
void print_all(Args&&... args)
{
// C++17 unary right fold over the comma operator
((std::cout << args << " "), ...);
std::cout << "\n";
}
template<typename... Args>
auto sum(Args... args)
{
// Fold expression over operator+
return (args + ...);
}
int main()
{
print_all(1, "two", 3.0, 'x');
std::cout << sum(1, 2, 3, 4) << "\n"; // 10
}CRTP — The Curiously Recurring Template Pattern
CRTP is a pattern, not a language feature: a class derives from a base class template, and instantiates that base with itself as the template argument — Derived : Base<Derived>. This gives the base class a way to call back into the derived class's specific methods, resolved entirely at compile time, with none of the runtime overhead (a vtable lookup) that virtual functions require.
Shape<Derived>::area() calls static_cast<const Derived*>(this)->compute_area() — a compile-time-resolved call to whichever compute_area the actual derived class defines. Because Circle inherits from Shape<Circle> and Square from Shape<Square>, each base class template instantiation is specialized to know exactly which derived type it is attached to. This is called static polymorphism: the call is dispatched at compile time (like an ordinary non-virtual call, and just as fast), whereas a virtual function achieves the same flexibility through a runtime vtable lookup.
#include <iostream>
template<typename Derived>
struct Shape
{
double area() const
{
// static_cast down to Derived is safe here specifically because
// Derived always publicly inherits from Shape<Derived>.
return static_cast<const Derived*>(this)->compute_area();
}
};
struct Circle : Shape<Circle>
{
double radius;
explicit Circle(double r) : radius(r) {}
double compute_area() const { return 3.14159265 * radius * radius; }
};
struct Square : Shape<Square>
{
double side;
explicit Square(double s) : side(s) {}
double compute_area() const { return side * side; }
};
template<typename T>
void report(const Shape<T>& shape)
{
std::cout << "area: " << shape.area() << "\n";
}
int main()
{
report(Circle(2.0));
report(Square(3.0));
}Template Metaprogramming and Compile-Time Computation
Templates are recursive and evaluated by the compiler, which means they form a small, genuinely Turing-complete programming language that runs entirely at compile time. The classic illustration is computing a factorial through template instantiation rather than a runtime loop.
Factorial<5>::value forces the compiler to instantiate Factorial<5>, which references Factorial<4>, which references Factorial<3>, and so on down to the full specialization Factorial<0>, which stops the recursion. The entire chain is resolved during compilation; by the time the program runs, Factorial<5>::value is just the constant 120 baked into the binary. static_assert then checks that compile-time fact and fails the build itself if it is ever wrong.
Modern C++ (C++11 onward) usually replaces this style of template metaprogramming with a plain constexpr function, which reads like ordinary code and is computed at compile time whenever its inputs allow it.
This computes the identical value, at the identical compile-time cost, but reads like an ordinary recursive function rather than a chain of struct specializations. Template metaprogramming in the struct-and-::value style is still essential for type-level computation — producing or transforming types rather than plain values — which is exactly the territory std::conditional, std::enable_if, and the rest of <type_traits> live in.
template<int N>
struct Factorial
{
static constexpr int value = N * Factorial<N - 1>::value;
};
template<>
struct Factorial<0>
{
static constexpr int value = 1;
};
static_assert(Factorial<5>::value == 120, "5! must be 120");constexpr int factorial(int n)
{
return n <= 1 ? 1 : n * factorial(n - 1);
}
static_assert(factorial(5) == 120);A Preview of Type Traits
Everything on this page — full and partial specialization, SFINAE, and compile-time computation — is exactly the machinery <type_traits> is built from. std::is_pointer<T>, std::is_same<T, U>, and dozens of similar traits are themselves small template structs that use partial specialization to answer a yes/no question about T at compile time.
std::is_pointer is implemented with essentially the same trick as TypeName<T*> earlier on this page: a generic template that inherits from false_type, and a partial specialization matching T* that inherits from true_type instead. The next topic covers the standard trait library in full.
#include <type_traits>
static_assert(std::is_pointer<int*>::value == true);
static_assert(std::is_pointer<int>::value == false);Best Practices and the Mental Model
Prefer std::enable_if and detection idioms only when concepts are unavailable; on C++20 and later, concepts and requires clauses say the same thing more clearly and produce far better error messages. Prefer constexpr functions over struct-based template metaprogramming whenever you are just computing a value rather than a type. Reach for CRTP specifically when you want compile-time-resolved polymorphism without virtual-call overhead, most often in performance-sensitive library code — not as a default substitute for ordinary inheritance. Use static_assert liberally to turn a wrong template instantiation into a clear compile error with a message, rather than a wall of substitution failures or, worse, silently wrong behavior.
The mental model underneath all of it: a template is a recipe the compiler follows to generate real code once it knows the actual types involved, and every technique here — specialization, SFINAE, concepts, variadics, CRTP, metaprogramming — is a way of controlling exactly which code that recipe produces, and when.
What is the difference between a full specialization and a partial specialization of a class template, and which one is available for function templates?
A full specialization fixes every template parameter to one concrete type and provides a complete alternate definition for exactly that instantiation. A partial specialization matches a pattern covering a whole family of types (such as any pointer type T*) while leaving some part still generic. Only class templates support partial specialization; function templates do not, and use ordinary overloading instead.
What does SFINAE stand for, and in what specific location must a substitution failure occur for SFINAE to apply instead of a hard compile error?
SFINAE stands for Substitution Failure Is Not An Error. It only applies to failures that occur while substituting template arguments into a function's declaration (its template parameters, function parameters, or return type) — its 'immediate context.' A failure that only manifests inside the function body once instantiated is a normal, hard compile error.
How does the void_t-based detection idiom determine whether a type T has a .size() member, and why does std::declval<T>() get used instead of an actual T object?
A primary template inherits from false_type by default, and a partial specialization using std::void_t<decltype(std::declval<T>().size())> as its second parameter only matches when that decltype expression is well-formed — i.e., when T actually has .size(). If it is well-formed, void_t collapses it to void and the true_type specialization is selected via partial-specialization matching; otherwise SFINAE discards that specialization. std::declval<T>() produces a fake T value usable only in unevaluated contexts like decltype, so the check works even for types that are not default-constructible.
In the CRTP pattern struct Derived : Base<Derived>, why is it safe for Base<Derived>::someMethod() to static_cast<Derived*>(this), and how does the performance of that call compare to a virtual function call?
It is safe because CRTP guarantees, by the pattern itself, that Base<Derived> is only ever used as a base of the specific class Derived — so any Base<Derived>* that reaches someMethod() genuinely does point at a Derived object, or at something derived further from it. The call is resolved entirely at compile time, like an ordinary non-virtual member call, so it has no vtable lookup and no runtime dispatch overhead, unlike a virtual function call.
What problem do C++20 concepts solve that std::enable_if-based SFINAE does not solve well, and what is the practical difference in how a caller experiences a failed constraint?
Concepts give a compile-time constraint a readable name and check it directly, rather than relying on a substitution failure being silently discarded from an overload set. When a call does not satisfy a concept, the compiler reports that the named concept (e.g. Numeric) was not satisfied for the given type; a failed enable_if-based SFINAE constraint instead typically produces a long, hard-to-read cascade of substitution-failure or overload-resolution errors with no single clear culprit named.
Write a C++ program that defines a class template Box<T> holding a value of type T, along with a full specialization Box<const char*> that stores and prints the string differently (e.g., wrapped in quotes) from the generic version. Demonstrate both in main().
Box<int> uses the generic template and prints Box(42). Box<const char*> matches the full specialization template<> class Box<const char*> instead, whose print() wraps the string in escaped quotes, producing Box("hello"). The compiler chooses between the two purely based on the template argument, at compile time, with no runtime branching involved.
What does this program print, and why doesn't the compiler consider the two handle() overloads ambiguous even though both are templates?
It prints "value: 10" and then "pointer, points to: 10". handle(x) instantiates T = int: the first overload's condition is_pointer<int>::value is false, so its enable_if has no ::type and it is dropped by SFINAE, leaving only the second overload. handle(&x) instantiates T = int*: the opposite happens, leaving only the first overload. Because the two enable_if conditions are exact logical negations of each other, exactly one overload ever survives substitution for any given T, so there is never a real ambiguity — one candidate is always eliminated before overload resolution even has to choose.
Write a C++20 program that defines a concept Printable requiring that a value of type T can be streamed to std::cout (using a requires-expression), and a template function show(T value) constrained by that concept. Demonstrate it with an int and a std::string.
The requires-expression requires(T value, std::ostream& os) { os << value; } declares that a type T only satisfies Printable if the expression os << value actually compiles for it. Both int and std::string support operator<< into an ostream, so template<Printable T> void show(T value) accepts both calls, printing "value: 42" and "value: engineering". A type without operator<< support would fail to satisfy Printable, and the compiler would report that specifically rather than emitting a wall of unrelated errors.
What does count_args(1, 2.0, "three", 'f') print, and what does sizeof...(args) actually count — the values passed in, or something else?
It prints 4. sizeof...(args) (equivalently sizeof...(Ts)) counts the number of elements in the parameter pack — here, the four arguments 1, 2.0, "three", and 'f' — regardless of what types they are. It is a compile-time constant, computed purely from how many arguments were passed to that specific instantiation, not from inspecting any runtime value.
#include <iostream>
#include <type_traits>
template<typename T>
typename std::enable_if<std::is_pointer<T>::value, void>::type
handle(T value)
{
std::cout << "pointer, points to: " << *value << "\n";
}
template<typename T>
typename std::enable_if<!std::is_pointer<T>::value, void>::type
handle(T value)
{
std::cout << "value: " << value << "\n";
}
int main()
{
int x = 10;
handle(x);
handle(&x);
}#include <iostream>
template<typename... Ts>
constexpr int count_args(Ts... args)
{
return sizeof...(args);
}
template<typename... Ts>
auto largest(Ts... args)
{
auto best = (args, ...); // placeholder, replaced below in explanation
return best;
}
int main()
{
std::cout << count_args(1, 2.0, "three", 'f') << "\n";
}