Lesson 17 • Advanced
Templates Deep Dive
By the end of this lesson you'll be able to write functions that take any number of arguments, customise a template for one specific type, constrain templates so misuse fails with a clear message, and reach for CRTP and C++20 concepts — the techniques the whole Standard Library is built on.
What You'll Learn
- Write variadic templates with parameter packs (typename... Args)
- Collapse a whole pack in one line with C++17 fold expressions
- Customise a template with full and partial specialization
- Inspect types at compile time with <type_traits>
- Constrain templates with SFINAE / enable_if and C++20 concepts
- Use CRTP for zero-overhead static polymorphism
template<typename T> function and instantiating it. This lesson builds straight on top of that.💡 Real-World Analogy
Think of a template as a cookie cutter, and a parameter pack as a recipe that says "add as many toppings as the customer asks for". A fold expression is the single motion that stirs every topping in at once. Specialization is keeping one special cutter just for, say, gingerbread men, because the generic round cutter would ruin them. And concepts are the sign on the counter — "dough only, no rocks" — so an impossible order is refused politely at the door instead of jamming the machine deep inside. The machine only actually runs (compiles) when a real order comes in, which is why template errors show up at the moment of use, not when the recipe was written.
1. Variadic Templates & Fold Expressions
A variadic template accepts any number of arguments. You declare a parameter pack with typename... Args (the pack of types) and a matching Args... args (the pack of values). You can't index a pack directly — you expand it. The modern way (C++17) is a fold expression: (args + ...) applies + across the whole pack in one line. sizeof...(args) tells you how many items are in the pack, at compile time. Read this worked sum(...), run it, then you'll write your own.
Worked example: a variadic sum()
Read every comment, run it, and check the output matches.
#include <iostream>
using namespace std;
// A parameter pack: "Args..." means "zero or more types".
// "args..." is the matching pack of VALUES.
template <typename... Args> // Args is a TYPE pack
auto sum(Args... args) { // args is the VALUE pack
// Fold expression (C++17): expand the pack with the + operator.
// (args + ...) becomes a1 + (a2 + (a3 + ... )); one clean line.
return (args + ...);
}
// sizeof...(pack) counts how many items are in a pack (at comp
...Your turn. The program below is almost complete — fill in the two blanks marked ___ using the hints in the comments. One is an arithmetic fold, the other is a comma fold ((cout << args...), ...) that runs a statement once per item.
🎯 Your turn: fold a parameter pack
Fill in the ___ blanks, then check your output against the expected lines.
#include <iostream>
using namespace std;
// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
template <typename... Args>
auto multiplyAll(Args... args) {
// 1) Fold the pack with the * operator (like the sum() example,
// but multiplying). Pattern: (args OP ...)
return ___; // 👉 (args * ...)
}
template <typename... Args>
void printAll(Args... args) {
// 2) Comma fold: run "cout << one item << ' '" for EVERY item.
// Pattern: ((cout << args
...2. Specialization, Type Traits & SFINAE
Full specialization (template<> struct Describe<bool>) replaces the template for one exact type. Partial specialization (struct Describe<T*>) replaces it for a whole family — here, every pointer type. The header <type_traits> gives you compile-time questions about types, like is_integral_v<T>. SFINAE ("Substitution Failure Is Not An Error") with enable_if_t makes an overload simply not exist when its condition is false, so the compiler quietly picks the right one instead of compiling something wrong.
Worked example: specialization + traits + enable_if
See full vs partial specialization and SFINAE picking overloads.
#include <iostream>
#include <type_traits>
using namespace std;
// === Primary template (the general case) ===
template <typename T>
struct Describe {
static string text() { return "some object"; }
};
// === FULL specialization: one exact type, T is fixed to bool ===
template <>
struct Describe<bool> {
static string text() { return "a true/false flag"; }
};
// === PARTIAL specialization: a family of types — any pointer T* ===
template <typename T>
struct Describe<T*> {
static stri
...Remember the rule: only class/struct templates can be partially specialized. For functions, you write ordinary overloads (or use a concept) instead of a partial specialization.
3. CRTP & C++20 Concepts
CRTP (Curiously Recurring Template Pattern) is a class that inherits from a base templated on itself: struct Dog : Greeter<Dog>. The base can then call the derived class's methods with zero runtime cost — "static polymorphism", resolved entirely at compile time, no virtual table. Concepts (C++20) are named, readable constraints on types: concept Addable = requires(T a, T b) { a + b; };. Constrain a template with the concept name and misuse fails with a short, clear message instead of a wall of template errors.
Worked example: CRTP + a concept
Static polymorphism with no virtual tables, plus a readable constraint.
#include <iostream>
using namespace std;
// === CRTP: Curiously Recurring Template Pattern ===
// A base class is templated on the DERIVED class, so the base can
// call the derived methods directly — "static polymorphism", no
// virtual tables, all resolved at compile time.
template <typename Derived>
struct Greeter {
void greet() {
// Cast ourselves to the derived type, then call its name().
cout << "Hi, I am " << static_cast<Derived*>(this)->name() << endl;
}
};
stru
...Now you try. Define a Numeric concept from a type trait, then use it to constrain square() so only number types compile. Fill in the two blanks:
🎯 Your turn: constrain with a concept
Define the concept, apply it as the type bound, then run.
#include <iostream>
#include <type_traits>
using namespace std;
// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
// 1) Define a concept "Numeric" that is true when T is an arithmetic
// type (any int or float). Trait to use: is_arithmetic_v<T>.
template <typename T>
concept Numeric = ___; // 👉 is_arithmetic_v<T>
// 2) Constrain square() so ONLY numeric types compile.
// Put the concept name where the type bound goes.
template <___ T> // 👉 Numeri
...🔎 Deep Dive: errors surface at instantiation
A template is not fully checked when you write it — only when you use it with a concrete type. That moment is called instantiation. So a mistake inside a template appears at the call site that triggered it, often buried deep in the compiler's output.
Two habits tame this. Add a static_assert with a message at the top of a template to fail early and clearly. Better still, in C++20 attach a concept — the compiler then says "constraint not satisfied" right at the call, instead of erroring 40 lines deep inside the body.
template <typename T>
T half(T x) {
static_assert(is_arithmetic_v<T>, // clear, early failure
"half() needs a number");
return x / 2;
}
// half(string("hi")); // ❌ stops here with YOUR message, not a wall of textPro Tips
- 💡 Prefer fold expressions over recursive unpacking — one line, no base case, friendlier errors.
- 💡 Prefer concepts over enable_if in new code; keep
enable_if/<type_traits>for reading older libraries. - 💡 Partial specialization is class-only — overload functions, don't try to partially specialize them.
- 💡 Reach for CRTP when you want a reusable mixin with no virtual-call overhead.
Common Errors (and the fix)
- "function template partial specialization is not allowed": you tried to partially specialize a function. Use an overload, an
enable_if, or a concept instead — partial specialization is for class/struct templates only. - "pack expansion does not contain any unexpanded parameter packs": you forgot the
...when expanding. Write(args + ...)orf(args...), not(args +)/f(args). - "no matching function for call" with a huge SFINAE dump: the type didn't satisfy any
enable_ifoverload. Check the trait — e.g. you passed adoubleto anis_integral_v-only overload. - "constraints not satisfied" (C++20): the type failed a
concept. That's the concept doing its job — read which requirement failed and pass a conforming type. - "undefined reference" to a template: you put the template's body in a
.cppfile. Template definitions must live in the header so each use can be instantiated.
📋 Quick Reference
| Feature | Syntax | Notes |
|---|---|---|
| Parameter pack | template <typename... Args> | Zero or more types |
| Pack size | sizeof...(args) | Count, at compile time |
| Fold (arithmetic) | (args + ...) | Collapse pack with op |
| Fold (comma) | ((cout << args), ...) | Run a stmt per item |
| Full specialization | template <> struct X<bool> | One exact type |
| Partial specialization | struct X<T*> | A family (class-only) |
| Type trait | is_integral_v<T> | From <type_traits> |
| SFINAE guard | enable_if_t<cond, R> | Overload exists if cond |
| Concept (C++20) | concept C = requires(T a){ a+a; }; | Named constraint |
Frequently Asked Questions
Q: What exactly is a parameter pack?
A parameter pack is a single name that stands for zero or more template arguments. template <typename... Args> declares a TYPE pack; the matching args... in the function is the VALUE pack. You never index a pack directly — you expand it (with ... or a fold) or count it with sizeof...(Args).
Q: When do I use a fold expression instead of recursion?
Almost always, in C++17 and later. A fold like (args + ...) collapses a whole pack with one operator in a single line — no base-case overload, less code, and clearer error messages. Reach for recursion only when each element needs genuinely different handling that an operator can't express.
Q: What's the difference between full and partial specialization?
Full specialization pins every parameter to one exact type, e.g. template <> struct Describe<bool>. Partial specialization fixes a PATTERN, e.g. template <typename T> struct Describe<T*> covers every pointer type. Important gotcha: only class/struct templates can be partially specialized — for functions you overload instead.
Q: Is SFINAE/enable_if still worth learning now that concepts exist?
Concepts (C++20) are clearer and give better errors, so prefer them in new code. But enable_if and the <type_traits> helpers (is_integral_v, is_floating_point_v, ...) are everywhere in existing libraries and pre-C++20 codebases, so you'll read and maintain them for years. Learn both; write concepts.
Q: Why is CRTP useful if virtual functions already give polymorphism?
CRTP gives you 'static polymorphism' — the base class calls into the derived class, but every call is resolved at compile time. That means no virtual-table lookup and the calls can be inlined, so it's used in performance-critical mixins. The trade-off: the type is fixed at compile time, so you can't store mixed CRTP objects behind one base pointer the way virtual functions allow.
Q: Why do template errors point at the call, not the template?
A template is only fully type-checked when it is INSTANTIATED — that is, when you use it with a concrete type. So a mistake inside the template surfaces at the line that triggered the instantiation. Read the error from the top, find the first message, and look at that call site; concepts and static_assert make this far less painful.
Mini-Challenge: count the bigger ones
No blanks this time — just a brief and an outline. Write a variadic countOver(threshold, values...) that returns how many values beat the threshold, using a comma fold over a counter. Check your output against the example in the comments.
🎯 Mini-Challenge: variadic countOver()
Write the variadic template yourself and call it from main.
#include <iostream>
using namespace std;
int main() {
// 🎯 MINI-CHALLENGE: a variadic count-the-bigger function
// 1. Write a variadic template countOver(threshold, values...)
// that returns how many of "values" are strictly greater than
// "threshold".
// 2. Hint: a fold over += with a comparison, e.g.
// int n = 0;
// ((values > threshold ? ++n : n), ...); // comma fold
// return n;
// 3. Call it from main and print the results.
...🎉 Lesson Complete
- ✅
typename... Argsdeclares a parameter pack; expand it, don't index it - ✅ Fold expressions like
(args + ...)collapse a whole pack in one line - ✅ Full specialization fixes one type; partial fixes a family (class-only)
- ✅
<type_traits>+enable_if(SFINAE) pick overloads by type - ✅ CRTP gives static polymorphism with no virtual-table cost
- ✅ C++20
conceptsare the modern, readable replacement for SFINAE - ✅ Template errors surface at instantiation — read them top-down
- ✅ Next lesson: STL Algorithm Mastery — transform, sort, find_if, and custom predicates
Sign up for free to track which lessons you've completed and get learning reminders.