Templates Deep Dive
Reviewed & published by Brayan K
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.
Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
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
💡 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.
#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 compile time).
template <typename... Args>
size_t howMany(Args... args) {
return sizeof...(args); // the count, NOT the byte size
}
int main() {
// The compiler stamps out a separate sum() for each call below.
cout << sum(1, 2, 3) << endl; // 6 (three ints)
cout << sum(1.5, 2.5, 4.0) << endl; // 8 (three doubles)
cout << sum(10) << endl; // 10 (a pack of one)
cout << howMany(1, 2, 3, 4, 5) << endl; // 5 (pack size)
return 0;
}
// ✅ Expected output:
// 6
// 8
// 10
// 5Your 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.
#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 << ' '), ...);
___; // 👉 ((cout << args << ' '), ...);
cout << endl;
}
int main() {
cout << multiplyAll(2, 3, 4) << endl; // expect: 24
printAll(1, 2, 3, 4); // expect: 1 2 3 4
// ✅ Expected output:
// 24
// 1 2 3 4
return 0;
}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.
#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 string text() { return "a pointer"; }
};
// === Type traits + SFINAE via enable_if ===
// This overload only EXISTS when T is an integer type. If the
// condition is false the overload silently drops out (SFINAE:
// "Substitution Failure Is Not An Error") instead of compiling wrong.
template <typename T>
enable_if_t<is_integral_v<T>, string> kind(T) {
return "integer";
}
// And this one only exists for floating-point types.
template <typename T>
enable_if_t<is_floating_point_v<T>, string> kind(T) {
return "decimal";
}
int main() {
cout << Describe<int>::text() << endl; // some object (primary)
cout << Describe<bool>::text() << endl; // a true/false flag (full)
cout << Describe<int*>::text() << endl; // a pointer (partial)
cout << kind(42) << endl; // integer (enable_if picks it)
cout << kind(3.14) << endl; // decimal
return 0;
}
// ✅ Expected output:
// some object
// a true/false flag
// a pointer
// integer
// decimalRemember 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.
#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;
}
};
struct Dog : Greeter<Dog> {
string name() { return "Rex (a dog)"; }
};
struct Cat : Greeter<Cat> {
string name() { return "Milo (a cat)"; }
};
// === C++20 concepts (brief): a named, readable constraint ===
// A concept is a compile-time predicate on a type. "Addable" means
// "you can write a + b for this type". The error if you break it is
// short and clear — far nicer than a raw template wall of text.
template <typename T>
concept Addable = requires(T a, T b) { a + b; };
template <Addable T> // only types that satisfy Addable compile
T twice(T x) { return x + x; }
int main() {
Dog{}.greet(); // Hi, I am Rex (a dog)
Cat{}.greet(); // Hi, I am Milo (a cat)
cout << twice(21) << endl; // 42
cout << twice(1.5) << endl; // 3
return 0;
}
// ✅ Expected output:
// Hi, I am Rex (a dog)
// Hi, I am Milo (a cat)
// 42
// 3Now 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:
#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> // 👉 Numeric
T square(T x) {
return x * x;
}
int main() {
cout << square(6) << endl; // expect: 36
cout << square(2.5) << endl; // expect: 6.25
// square("hi"); // would now FAIL clearly: constraint not satisfied
// ✅ Expected output:
// 36
// 6.25
return 0;
}🔎 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 + ...) or f(args...), not (args +) / f(args).
- "no matching function for call" with a huge SFINAE dump: the type didn't satisfy any enable_if overload. Check the trait — e.g. you passed a double to an is_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 .cpp file. 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 |
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.
#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.
//
// ✅ Expected output:
// countOver(5, 3, 8, 5, 9, 1) -> 2
// countOver(0, -1, 2, -3, 4) -> 2
// your code here
return 0;
}🎉 Lesson Complete
- ✅ typename... Args declares 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 concepts are the modern, readable replacement for SFINAE
- ✅ Template errors surface at instantiation — read them top-down
Practice quiz
What does template <typename... Args> declare?
- A single template type
- A variadic macro
- A parameter pack — a name standing for zero or more template arguments
- An array of types
Answer: A parameter pack — a name standing for zero or more template arguments. typename... Args declares a type parameter pack: it stands for zero or more types.
How do you work with a parameter pack?
- Expand it (with ... or a fold) or count it with sizeof...
- Index it like args[0], args[1]
- Loop over it with a for loop
- Convert it to a vector first
Answer: Expand it (with ... or a fold) or count it with sizeof.... You never index a pack directly — you expand it (... or a fold) or count it with sizeof...(Args).
What does the fold expression (args + ...) do?
- Adds 1 to every argument
- Appends args to a list
- Returns the number of arguments
- Collapses the whole pack with the + operator in one line
Answer: Collapses the whole pack with the + operator in one line. A fold expression like (args + ...) applies + across the entire pack, e.g. a1 + (a2 + (a3 + ...)).
What does sizeof...(args) return?
- The total bytes of all arguments
- The number of elements in the parameter pack, at compile time
- The size of the largest argument
- Always 1
Answer: The number of elements in the parameter pack, at compile time. sizeof...(pack) is the count of items in the pack, computed at compile time (not a byte size).
What is the difference between full and partial specialization?
- Full pins every parameter to one exact type; partial fixes a family/pattern like T*
- Full fixes a pattern; partial fixes one exact type
- They are the same thing
- Partial only works on functions
Answer: Full pins every parameter to one exact type; partial fixes a family/pattern like T*. Full specialization (template<> Describe<bool>) pins one exact type; partial (Describe<T*>) covers a family.
Which kinds of templates can be PARTIALLY specialized?
- Only function templates
- Any template
- Only class/struct templates — for functions you overload instead
- Only variable templates
Answer: Only class/struct templates — for functions you overload instead. Partial specialization is class/struct-only; functions are overloaded (or constrained) rather than partially specialized.
What does SFINAE stand for, and what does enable_if exploit it for?
- Standard Function In Namespace And Enum; to rename functions
- Substitution Failure Is Not An Error; to make an overload simply not exist when a condition is false
- Static Functions Are Never In Errors; to inline functions
- Single Format Is Not Always Easy; to format types
Answer: Substitution Failure Is Not An Error; to make an overload simply not exist when a condition is false. SFINAE = Substitution Failure Is Not An Error; enable_if uses it so an overload drops out when its condition is false.
Which <type_traits> helper is true only for integer types?
- is_floating_point_v<T>
- is_pointer_v<T>
- is_class_v<T>
- is_integral_v<T>
Answer: is_integral_v<T>. is_integral_v<T> is true for integer types (and false for double); is_floating_point_v<T> is the float counterpart.
What does CRTP (Curiously Recurring Template Pattern) provide?
- Runtime polymorphism via virtual tables
- Static (compile-time) polymorphism with no virtual-table cost
- Automatic memory management
- Variadic argument handling
Answer: Static (compile-time) polymorphism with no virtual-table cost. CRTP has a base templated on the derived type, so calls resolve at compile time with no virtual-table overhead.
Why do template errors often point at the call site rather than the template definition?
- The compiler has a bug
- Templates are never checked
- A template is only fully type-checked when it is instantiated with a concrete type
- Errors always point at line 1
Answer: A template is only fully type-checked when it is instantiated with a concrete type. Templates are checked at instantiation, so a mistake inside surfaces at the call that triggered it; concepts make this clearer.
Continue this course
- Previous: Advanced OOP in C++: Virtual Tables, Polymorphic Dispatch & Inheritance Trees
- Next: STL Algorithm Mastery: Transformations, Predicates & Custom Conditions — std::transform, sort, find_if, partition, and custom comparators
- Quick reference: C++ cheat sheet
Frequently asked questions
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).
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.
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.
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.
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.
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.