๐งฉ Why Templates Exist
You've already met this exact problem in disguise, back in Phase 3's function overloading. Templates are a genuinely more powerful solution to that same underlying issue.
int getMax(int a, int b) { return (a > b) ? a : b; } double getMax(double a, double b) { return (a > b) ? a : b; } std::string getMax(std::string a, std::string b) { return (a > b) ? a : b; } // the LOGIC is identical every time โ only the type actually changes
Phase 3's function overloading (Module 8) lets these share one NAME, but you still had to write the SAME logic three separate times, once per type. If you needed getMax for 10 different types, you'd write it 10 times โ genuinely wasteful, and a maintenance risk if you ever need to fix a bug in the logic.
template <typename T> T getMax(T a, T b) { return (a > b) ? a : b; } getMax(5, 3); // works for int getMax(5.5, 3.2); // works for double getMax(std::string("b"), std::string("a")); // works for string
A template lets you write ONE version of a function (or class, Module 3) that works for genuinely any type โ T is a placeholder the compiler fills in automatically based on what you actually call it with. One piece of logic, reused everywhere, with zero duplication.
std::vector<int>, std::map<std::string, int> โ the STL's ENTIRE container library is built from templates. std::vector<T> is a template that works identically whether T is int, std::string, or your own custom class. This phase teaches you to write that same kind of genuinely reusable code yourself.๐ง Function Templates
A closer, more careful look at exactly how the syntax from Module 1 works, piece by piece.
template <typename T> T getMax(T a, T b) { return (a > b) ? a : b; }
| Part | Meaning |
|---|---|
template <typename T> | "What follows is a template. T is a placeholder for a type, to be filled in later." |
T getMax(T a, T b) | A regular function declaration (Phase 3 Module 6), except T is used wherever a concrete type would normally go |
template <class T> in real code โ for this basic use, typename and class mean exactly the same thing. typename is generally considered the clearer, more modern convention.When you call getMax(5, 3), the compiler looks at the arguments, sees they're both int, and generates a real, concrete version of the function with T replaced by int โ as if you'd hand-written the int version yourself. Call it again with doubles, and it generates a SEPARATE concrete version for double. This process is called instantiation.
๐ฆ Class Templates
The same idea from Module 1, applied to entire classes instead of just functions โ exactly how std::vector itself is actually built.
template <typename T> class Box { private: T contents; public: Box(T value) : contents(value) {} T getContents() { return contents; } }; Box<int> intBox(42); Box<std::string> strBox("Hello"); std::cout << intBox.getContents(); // 42 std::cout << strBox.getContents(); // Hello
This is precisely Phase 5's class syntax, with template <typename T> added and T used wherever a specific type would normally appear. Box<int> and Box<std::string> are genuinely different, separate types once instantiated โ but they share one single piece of source code.
std::vector itself is essentially a (far more sophisticated) class template โ that's exactly why std::vector<int> and std::vector<std::string> both work identically, using the exact same underlying code, just instantiated for different types. You now understand the actual mechanism behind syntax you've been using since Phase 7.
๐ Template Type Deduction
You've been writing getMax(5, 3) without ever explicitly stating T is int. Here's exactly how the compiler figures that out.
getMax(5, 3); // compiler infers T = int getMax<int>(5, 3); // explicitly stating T = int โ identical result, rarely needed
Genuinely similar to Phase 8 Module 7's auto โ the compiler looks at the actual arguments you pass and deduces T automatically. You almost never need to write getMax<int>(...) explicitly; it's available when genuinely needed, but the implicit version is the normal, everyday style.
getMax(5, 3.5); // COMPILER ERROR โ T can't be BOTH int and double at once
template <typename T> T getMax(T a, T b) requires a and b to both deduce to the exact same T โ mixing int and double gives the compiler no single consistent type to use, and it refuses to guess. Module 5 covers how to genuinely support different types for different parameters, when that's actually what you need.๐ฅ Multiple Template Parameters
The fix for last module's trap โ genuinely supporting different types for different parameters, when that's actually what you need.
template <typename T1, typename T2> void printPair(T1 first, T2 second) { std::cout << first << " and " << second << std::endl; } printPair(42, "apples"); // T1 = int, T2 = const char* โ genuinely different types, no problem
You can declare as many independent placeholder types as you genuinely need, separated by commas. Each parameter's type is deduced independently โ this directly fixes Module 4's mismatched-type trap, since T1 and T2 are no longer forced to match.
template <typename T1, typename T2> class Pair { public: T1 first; T2 second; };
std::pair<std::string, int> from Phase 7 Module 9 is precisely this pattern โ two independent template parameters, letting the key and value hold genuinely different types. std::map<K, V> follows the exact same idea underneath.
๐ฏ Template Specialization
Sometimes the generic, one-size-fits-all template logic genuinely isn't right for one specific type. Specialization lets you carve out an exception.
Suppose a generic describe(T value) template prints a value directly โ that works fine for numbers, but for a bool, printing raw 1 or 0 is far less readable than printing "true" or "false". Template specialization lets you write a genuinely different implementation specifically for one particular type, while every OTHER type still uses the generic version.
template <typename T> void describe(T value) { std::cout << "Value: " << value << std::endl; } template <> // empty <> means this is a full specialization void describe<bool>(bool value) { std::cout << "Value: " << (value ? "true" : "false") << std::endl; } describe(42); // uses the generic version โ "Value: 42" describe(true); // uses the bool specialization โ "Value: true"
The compiler always prefers a specialization that exactly matches the type being used โ describe(true) genuinely calls the bool version, while everything else falls through to the generic template.
๐ Templates and the STL
A direct look back at Phase 7 with this phase's new understanding โ connecting everything you've learned about templates to syntax you've already used for weeks.
| Phase 7 Syntax | What's Genuinely Happening |
|---|---|
std::vector<int> | The class template vector, instantiated with T = int |
std::map<std::string, int> | The class template map, instantiated with Key = std::string, Value = int โ Module 5's multiple parameters |
std::pair<std::string, int> | Genuinely, structurally almost identical to Module 5's own Pair example |
template <typename Iterator> void sort(Iterator begin, Iterator end) { // sorting logic that works for ANY container's iterators }
This is exactly why Phase 7 Module 7's std::sort worked identically on a vector<int> and could equally work on a vector<std::string> or your own Box<T> objects (if they supported comparison) โ one function template, genuinely reused across countless different types.
template <typename T> pattern from Module 1, applied at a genuinely large, professional scale.๐ Capstone โ A Generic Stack
Every idea from this phase, combined: a genuinely reusable class template, built on Phase 7's vector and Phase 8's move semantics, working identically for any type.
Build a Stack<T> class template โ last-in-first-out storage โ with push, pop, and top operations, that works identically whether T is int, std::string, or a custom class, without writing it more than once.
#include <iostream> #include <vector> #include <stdexcept> template <typename T> class Stack { private: std::vector<T> items; public: void push(T value) { items.push_back(value); } void pop() { if (items.empty()) { throw std::runtime_error("Cannot pop from an empty stack"); } items.pop_back(); } T& top() { if (items.empty()) { throw std::runtime_error("Cannot access top of an empty stack"); } return items.back(); } bool empty() { return items.empty(); } }; int main() { Stack<int> intStack; intStack.push(1); intStack.push(2); intStack.push(3); std::cout << "Top: " << intStack.top() << std::endl; // 3 intStack.pop(); std::cout << "Top after pop: " << intStack.top() << std::endl; // 2 Stack<std::string> strStack; // the SAME class template, a completely different type strStack.push("hello"); std::cout << "String stack top: " << strStack.top() << std::endl; return 0; }
(throw std::runtime_error(...) is a preview of Phase 10's exception handling โ for now, just recognize it as "signal that something went genuinely wrong.")
std::vector<T>internally handles the actual storage โ Module 6's insight that STL containers are themselves templates, reused here directly rather than reinventing dynamic storage.- Both
pop()andtop()checkempty()first โ Phase 5 Module 6's encapsulation/validation principle, protecting the class from an invalid operation. Stack<int>andStack<std::string>are genuinely separate types (Module 3), yet came from writing this class exactly once.
Add a size() Method and a Max-Capacity Check
Add a size() method returning the current element count, and modify the constructor to accept an optional maximum capacity โ push() should throw if the stack is already full. This combines Phase 5's constructor defaults (Module 7) with this phase's template class, genuinely testing whether you can integrate ideas across multiple phases, not just repeat this lesson's exact pattern.