Templates in C++ let you write type-safe, reusable code that the compiler specializes for the types you use, often with zero runtime overhead. This beginner-friendly guide covers function and class templates with clear examples, outputs, and “Try it yourself” challenges after each section.
🔹 What are Templates in C++?
Templates in C++ enable generic programming: you write an algorithm once and the compiler generates optimized versions for each type you use. This gives you the flexibility of “generic” code with the performance of hand-written, type-specific code.
- Function templates: generic functions parameterized by type.
- Class templates: generic types (containers, math types, adapters).
- Non-type parameters: constants as template parameters (sizes, flags).
- Specialization: customize behavior for specific types.
- Concepts (C++20): add readable constraints to templates.
Try it yourself
- Name two functions you’ve duplicated for
int
anddouble
. Could they be a single function template? - List one class (e.g.,
Box
) that could become a class template with different stored types.
🔹 Function Templates: The Basics
Function templates look like regular functions, but with a template<typename T>
parameter that represents a type. The compiler deduces T
from arguments at the call site and generates the concrete function for you.
#include <iostream>
#include <string>
using namespace std;
// A generic add that works for any T supporting operator+
template <typename T>
T add(T a, T b) {
return a + b; // must be valid for T
}
int main() {
cout << add(2, 3) << "\n"; // T = int
cout << add(2.5, 1.2) << "\n"; // T = double
cout << add(string("Hi "), string("C++"))
<< "\n"; // T = std::string
}
Output
5
3.7
Hi C++
The compiler instantiates add<int>
, add<double>
, and add<std::string>
at compile time. No virtual calls, no runtime overhead.
Try it yourself
- Create
maxOf(T a, T b)
that returns the larger value. - Write
swapValues(T&, T&)
and test withint
,double
, andstd::string
.
🔹 Class Templates: Generic Types
Class templates parameterize types themselves. Think of std::vector<T>
: it’s one class template, instantiated for many types like vector<int>
, vector<double>
, etc.
#include <iostream>
#include <utility>
using namespace std;
template <typename T>
class Box {
T value;
public:
Box() = default;
explicit Box(T v) : value(std::move(v)) {}
void set(T v) { value = std::move(v); }
const T& get() const { return value; }
};
int main() {
Box<int> bi(42);
Box<string> bs(string("templates"));
cout << bi.get() << "\n";
cout << bs.get() << "\n";
bi.set(100);
cout << bi.get() << "\n";
}
Output
42
templates
100
Each instantiation generates a distinct type: Box<int>
is different from Box<string>
, so type safety stays strong.
Try it yourself
- Add
void print() const
toBox
that prints the value. Test withint
andstd::string
. - Create
Pair<T, U>
storing two different types; addfirst()
andsecond()
methods.
🔹 Overload Resolution and Deduction
When both template and non-template overloads match, non-template overloads have priority. Also, type deduction must succeed; otherwise you may need explicit template arguments or conversions.
#include <iostream>
using namespace std;
void show(int x) { cout << "non-template: " << x << "\n"; }
template <typename T>
void show(T x) { cout << "template: " << x << "\n"; }
int main() {
show(5); // prefers non-template
show(3.14); // template version (no non-template match)
show<const char*>("hi"); // explicit template arg
}
Output
non-template: 5
template: 3.14
template: hi
Remember: template argument deduction must produce a unique type, or you’ll get ambiguity or compilation errors.
Try it yourself
- Add another non-template
show(double)
and see which overload is picked for3.14
. - Call
show('A')
and observe which overload is chosen on your compiler.
🔹 Template Specialization (Function and Class)
Specialization lets you customize behavior for specific types while keeping a generic default. Use full specialization for a specific type. Class templates also support partial specialization (based on patterns), but function templates do not.
#include <iostream>
#include <string>
using namespace std;
// Generic print template
template <typename T>
void print(const T& x) { cout << x << "\n"; }
// Full specialization for bool
template <>
void print<bool>(const bool& x)
{ cout << (x ? "true" : "false") << "\n"; }
// Class template + partial specialization
template <typename T>
struct Holder {
T value;
void dump() const
{ cout << "Holder<T>: "
<< value << "\n";
}
};
// Partial specialization for pointer types
template <typename T>
struct Holder<T*> {
T* value{};
void dump() const
{ cout << "Holder<T*>: "
<< (value ? "ptr set" : "null") << "\n";
}
};
int main() {
print(10); // generic
print(true); // specialized for bool
Holder<int> h1{42};
Holder<int*> h2{nullptr};
h1.dump();
h2.dump();
}
Output
10
true
Holder<T>: 42
Holder<T*>: null
Use specialization to tweak behavior for special cases without duplicating the whole implementation.
Try it yourself
- Add a full specialization
print<std::string>
that quotes the string. - Create a partial specialization
Holder<const T>
that prints “const T”.
🔹 Non-Type Template Parameters (NTTP)
Templates can take values (like integers) as parameters. This is useful for sizes and policies known at compile time, enabling optimizations and stronger type checks.
#include <array>
#include <iostream>
using namespace std;
template <typename T, size_t N>
struct StaticBuffer {
array<T, N> data{};
constexpr size_t size() const
{ return N; }
T& operator[](size_t i)
{ return data[i]; }
const T& operator[](size_t i) const
{ return data[i]; }
};
int main() {
StaticBuffer<int, 4> buf;
for (size_t i = 0; i < buf.size(); ++i)
buf[i] = static_cast<int>(i * 10);
for (size_t i = 0; i < buf.size(); ++i)
cout << buf[i] << " ";
cout << "\n";
}
Output
0 10 20 30
N
is part of the type: StaticBuffer<int, 4>
and StaticBuffer<int, 8>
are different types, preventing accidental mismatch in APIs.
Try it yourself
- Create
Matrix<T, R, C>
with fixed rows/cols and addmultiply
for compatible sizes. - Add a boolean NTTP (e.g.,
bool Debug
) to toggle logging at compile time.
🔹 Concepts (C++20): Constraining Templates
Concepts let you express requirements on template parameters with readable errors and cleaner overloads. They replace many SFINAE tricks with clear syntax.
#include <concepts>
#include <iostream>
using namespace std;
template <typename T>
concept Number = std::integral<T> || std::floating_point<T>;
template <Number T>
T safe_add(T a, T b) {
return a + b;
}
int main() {
cout << safe_add(2, 3)
<< "\n"; // OK (integral)
cout << safe_add(2.5, 1.5)
<< "\n"; // OK (floating)
// safe_add(string("a"), string("b")); // ERROR: doesn't satisfy Number
}
Output
5
4
With concepts, you get descriptive error messages when constraints aren’t met, making Templates in C++ more approachable.
Try it yourself
- Define a concept
HasSize
requiringobj.size()
, then writeprintSize
for anyHasSize
type. - Create two constrained overloads of
sum
for integral vs floating types and print a different label.
🔹 Variadic Templates and Fold Expressions
Variadic templates accept any number of template arguments. Fold expressions (C++17) make operations over parameter packs concise and readable.
#include <iostream>
using namespace std;
// Sum arbitrary many numbers using a fold expression
template <typename... Args>
auto sum(Args... args) {
return (args + ...); // (((a + b) + c) + ...)
}
int main() {
cout << sum(1, 2, 3, 4)
<< "\n";
cout << sum(1.5, 2.0, 3.5)
<< "\n";
}
Output
10
7
Variadics are powerful for logging, tuple utilities, or adapter layers where argument counts vary.
Try it yourself
- Write
printAll(args...)
that prints values separated by commas using a fold over((cout << args << ", "), ...)
. - Implement
allTrue(args...)
with a logical AND fold.
🔹 Templates and Separate Compilation
Templates must be visible at the point of use (typically in headers). If you put definitions only in .cpp
files, you’ll hit linker errors. Options: keep definitions in headers or use explicit instantiation declarations/definitions.
// math.hpp
#pragma once
template <typename T>
T square(T x) { return x * x; } // define in header so callers can instantiate
// main.cpp
#include <iostream>
#include "math.hpp"
int main() {
std::cout << square(7)
<< "\n";
}
Advanced: explicit instantiation lets you keep definitions in a .cpp
but only for specific types you declare (trade flexibility for compile speed and smaller binaries).
Try it yourself
- Create
stats.hpp
withmean(T*, size_t)
in the header and include it in two different.cpp
files. Build and run. - Research and try
extern template
/ explicit instantiation in one TU fordouble
.
🔹 Mini Project: Generic 2D Vector with Concepts
Combine class templates, operator overloading, and concepts to build a small math type. This showcases practical Templates in C++ with readable constraints.
#include <concepts>
#include <iostream>
#include <cmath>
using namespace std;
template <typename T>
concept Arithmetic = std::integral<T> || std::floating_point<T>;
template <Arithmetic T>
struct Vec2 {
T x{}, y{};
Vec2() = default;
Vec2(T x_, T y_) : x(x_), y(y_) {}
Vec2 operator+(const Vec2& other) const
{ return {x + other.x, y + other.y}; }
Vec2 operator-(const Vec2& other) const
{ return {x - other.x, y - other.y}; }
Vec2 operator*(T k) const
{ return {x * k, y * k}; }
T dot(const Vec2& other) const
{ return x * other.x + y * other.y; }
double magnitude() const {
return std::sqrt(static_cast<double>(x) * x +
static_cast<double>(y) * y);
}
};
template <Arithmetic T>
ostream& operator<<(ostream& os,
const Vec2<T>& v) {
return os << "(" << v.x
<< ", " << v.y << ")";
}
int main() {
Vec2<int> a(2, 3), b(4, 1);
Vec2<double> c(1.5, 2.5);
cout << (a + b) << "\n";
cout << (a - b) << "\n";
cout << (c * 2.0) << "\n";
cout << "dot(a,b)="
<< a.dot(b) << "\n";
cout << "||c||="
<< c.magnitude() << "\n";
}
Output
(6, 4)
(-2, 2)
(3, 5)
dot(a,b)=11
||c||=2.91548
This template is reusable for any arithmetic type and benefits from compile-time checks and inlining for performance.
Try it yourself
- Add
normalize()
returning a unit vector (guard against zero length). - Implement
operator==
with a tolerance for floating-point types.
🔹 Best Practices and Common Pitfalls
- Keep template definitions in headers (or use explicit instantiation) to avoid link errors.
- Use concepts (C++20) or
static_assert
to provide clear diagnostics and constrain templates. - Avoid overly generic templates that accept types they can’t handle—prefer precise constraints.
- Beware of code bloat from many template instantiations; consider type-erasure/virtual interfaces if needed.
- Prefer
auto
and CTAD (Class Template Argument Deduction) when available to reduce verbosity. - Document expectations: required operations, complexity notes, and supported types.
Try it yourself
- Add
static_assert
inside a template to check constraints (e.g.,std::is_arithmetic_v<T>
). - Experiment with explicit instantiation of a frequently used template to speed up builds.
🔹 FAQs about Templates in C++
Q1: Are templates runtime-polymorphic?
No. They’re compile-time polymorphism. The compiler generates concrete code for the types you use—no vtable overhead.
Q2: Where should I put template definitions?
In headers, so callers can instantiate them. Alternatively, use explicit instantiation (advanced) in a .cpp
.
Q3: Can function templates be partially specialized?
No. Only full specialization is allowed for function templates. Class templates support partial specialization.
Q4: What are concepts and why use them?
Concepts (C++20) are readable constraints for templates. They improve error messages and help select the right overloads.
Q5: Do templates increase binary size?
They can due to multiple instantiations. Mitigate with fewer type variants, explicit instantiation, or type-erasure when appropriate.
Q6: Can I specialize a member function of a class template?
Yes, but rules are nuanced. Often you partially specialize the class instead of individual members for clarity.
Q7: What’s SFINAE?
“Substitution Failure Is Not An Error” is a technique to exclude invalid template overloads during substitution. Concepts offer a cleaner alternative.
Q8: How do variadic templates help?
They let you handle a variable number of types/arguments. Fold expressions (C++17) make them concise.
Q9: What is CTAD?
Class Template Argument Deduction lets the compiler deduce template parameters from constructor arguments (e.g., std::pair p(1, 2.0)
).
Q10: Templates vs virtual interfaces—when to choose which?
Use templates for performance and when types are known at compile time; use virtual interfaces for runtime pluggability and ABI stability.
🔹 Wrapping Up
Templates in C++ power safe, reusable, and fast code for both functions and classes. With specialization, NTTPs, variadics, and concepts, you can model rich, type-safe APIs without runtime cost. Practice the “Try it yourself” tasks to internalize these patterns and apply them confidently in your projects.