Lesson 159 min read

Templates

Cookie cutters for code β€” one shape, any dough. Write once, stamp out versions for every type

The Cookie Cutter

Imagine a star-shaped cookie cutter. It doesn't care if you use chocolate dough, sugar dough, or gingerbread β€” same shape, different material. That's a template in C++.

Instead of writing a separate maxInt(), maxDouble(), maxString(), you write one template function and let the compiler stamp out the right version for each type you actually use. Zero runtime cost β€” it's all resolved at compile time.

Function Templates

A function template is a blueprint. You write it with a placeholder type, and the compiler generates concrete functions as needed.

Template max Function

#include <iostream>
#include <string>
using namespace std;
// One function, works with any type that supports >
template <typename T>
T myMax(T a, T b) {
return (a > b) ? a : b;
}
int main() {
cout << myMax(3, 7) << endl; // int version
cout << myMax(3.14, 2.72) << endl; // double version
cout << myMax<string>("apple", "banana") << endl; // string version
// Explicit type when needed:
cout << myMax<double>(3, 2.5) << endl; // force double
return 0;
}
Output
7
3.14
banana
3

Template Type Deduction

Usually you don't need to spell out the type β€” the compiler deduces it from the arguments:

  • myMax(3, 7) β€” compiler sees two ints, stamps out myMax<int>
  • myMax(3.14, 2.72) β€” two doubles, stamps out myMax<double>

But when types conflict (myMax(3, 2.5) β€” int and double), you need to specify explicitly: myMax<double>(3, 2.5).

Class Templates

Templates aren't just for functions β€” you can template entire classes. This is how vector<int>, map<string, int>, and every other STL container works.

Template Stack Class

#include <iostream>
#include <vector>
#include <stdexcept>
using namespace std;
template <typename T>
class Stack {
vector<T> data;
public:
void push(const T& val) { data.push_back(val); }
T pop() {
if (data.empty()) throw runtime_error("Stack is empty");
T top = data.back();
data.pop_back();
return top;
}
T peek() const {
if (data.empty()) throw runtime_error("Stack is empty");
return data.back();
}
bool empty() const { return data.empty(); }
size_t size() const { return data.size(); }
};
int main() {
Stack<int> intStack;
intStack.push(10);
intStack.push(20);
intStack.push(30);
cout << "Top: " << intStack.peek() << endl;
cout << "Pop: " << intStack.pop() << endl;
cout << "Size: " << intStack.size() << endl;
Stack<string> strStack;
strStack.push("Hello");
strStack.push("World");
cout << "String top: " << strStack.peek() << endl;
return 0;
}
Output
Top: 30
Pop: 30
Size: 2
String top: World

Template Specialization

Sometimes the generic version doesn't work well for a specific type. You can specialize β€” provide a custom implementation for that type while keeping the generic version for everything else.

Specialization for string

#include <iostream>
#include <string>
using namespace std;
// Generic version
template <typename T>
class Printer {
public:
void print(const T& val) {
cout << "Value: " << val << endl;
}
};
// Specialization for string: add quotes
template <>
class Printer<string> {
public:
void print(const string& val) {
cout << "String: \"" << val << "\"" << endl;
}
};
// Specialization for bool: print true/false
template <>
class Printer<bool> {
public:
void print(const bool& val) {
cout << "Bool: " << (val ? "true" : "false") << endl;
}
};
int main() {
Printer<int> ip;
ip.print(42);
Printer<string> sp;
sp.print("hello");
Printer<bool> bp;
bp.print(true);
return 0;
}
Output
Value: 42
String: "hello"
Bool: true

auto with Templates (C++14+)

#include <iostream>
#include <vector>
using namespace std;
// C++14: auto return type deduction
template <typename T, typename U>
auto add(T a, U b) {
return a + b; // compiler figures out the return type
}
// C++20: abbreviated function template (auto parameters)
auto multiply(auto a, auto b) {
return a * b;
}
int main() {
cout << add(1, 2.5) << endl; // int + double = double
cout << add(string("Hi "), string("there")) << endl;
cout << multiply(3, 4) << endl; // int * int = int
cout << multiply(2.5, 4) << endl; // double * int = double
return 0;
}
Output
3.5
Hi there
12
10

Variadic Templates (Brief)

C++11 introduced variadic templates β€” templates that accept any number of arguments. This is how std::tuple, std::make_unique, and printf-like functions work.

template <typename... Args>
void print(Args... args) {
    ((cout << args << " "), ...);
    cout << endl;
}
print(1, "hello", 3.14);  // 1 hello 3.14

The ... is a parameter pack. The fold expression ((cout << args << " "), ...) expands it for each argument.

C++20 Concepts (Brief)

Concepts let you constrain template parameters so that you get clear error messages instead of pages of cryptic template errors:

template <typename T>
  requires std::integral<T>
T gcd(T a, T b) { return b == 0 ? a : gcd(b, a % b); }

gcd(12, 8);    // OK: int is integral
// gcd(1.5, 2.0); // ERROR: clear message about double not being integral

Concepts are one of the biggest quality-of-life improvements in modern C++.

SFINAE (Mention)

Before concepts, C++ used SFINAE (Substitution Failure Is Not An Error) β€” a rule where if a template instantiation fails, the compiler silently tries other overloads instead of erroring. It works, but the syntax is ugly. If you're writing C++20 or later, prefer concepts.

Note: Templates are compiled only when used β€” errors appear at the call site, not the definition. This makes template error messages notoriously cryptic (sometimes hundreds of lines for one mistake). C++20 concepts help enormously by constraining types upfront and giving clear, human-readable errors.
Challenge

Quick check

When does the compiler generate code from a template?
← Iterators & Algorithms