C++ Templates & Generic Programming: Function Templates, Class Templates, Specialization & SFINAE (C++23)

C++ Templates & Generic Programming: Function Templates, Class Templates, Specialization & SFINAE (C++23)
Table of Contents
- How Template Instantiation Works
- Function Templates: Type Deduction Rules
- Class Templates: Building Generic Containers
- Non-Type Template Parameters (NTTPs)
- Template Specialization: Full and Partial
- Variadic Templates and Fold Expressions
- if constexpr: Compile-Time Branching (C++17)
- SFINAE and std::enable_if: Constraining Templates
- Variable Templates (C++14)
- Template Instantiation and Code Bloat
- Frequently Asked Questions
- Key Takeaway
How Template Instantiation Works
Templates are not compiled until instantiated. The compiler generates a fresh, type-specific version for each unique set of type arguments. Each version is a fully independent function — no virtual dispatch, no boxing, full inlining.
Function Templates: Type Deduction Rules
#include <concepts>
#include <string>
// Basic function template
template<typename T>
T my_max(T a, T b) {
return (a > b) ? a : b;
}
// Type deduction — no explicit type needed at call site:
auto x = my_max(10, 20); // T = int (deduced)
auto y = my_max(3.14, 2.71); // T = double (deduced)
auto z = my_max<long>(10, 20LL); // T = long (explicit)
// my_max(10, 3.14); // ERROR: ambiguous — T must be ONE type
// Multiple type parameters:
template<typename T, typename U>
auto add(T a, U b) -> decltype(a + b) { // Trailing return type
return a + b;
}
auto r = add(10, 3.14); // returns double
// C++20 abbreviated function templates (auto parameters):
auto multiply(auto a, auto b) { return a * b; }
// Equivalent to: template<typename T, typename U> auto multiply(T a, U b)
// Constrained template (Concepts — covered in next module):
template<std::integral T>
T gcd(T a, T b) {
while (b) { a %= b; std::swap(a, b); }
return a;
}
gcd(12, 8); // OK: int satisfies std::integral
// gcd(1.5, 2.5); // COMPILE ERROR: double doesn't satisfy integralClass Templates: Building Generic Containers
#include <stdexcept>
#include <memory>
// Generic stack — works with any type
template<typename T, size_t MaxSize = 128>
class Stack {
T data_[MaxSize];
int top_ = -1;
public:
void push(const T& val) {
if (top_ >= static_cast<int>(MaxSize) - 1)
throw std::overflow_error("Stack full");
data_[++top_] = val;
}
void push(T&& val) { // Move overload
if (top_ >= static_cast<int>(MaxSize) - 1)
throw std::overflow_error("Stack full");
data_[++top_] = std::move(val);
}
template<typename... Args>
T& emplace(Args&&... args) { // Construct in place
if (top_ >= static_cast<int>(MaxSize) - 1)
throw std::overflow_error("Stack full");
return data_[++top_] = T(std::forward<Args>(args)...);
}
void pop() { if (!empty()) --top_; }
T& top() { return data_[top_]; }
bool empty() const { return top_ < 0; }
int size() const { return top_ + 1; }
};
// Instantiation with default and custom sizes:
Stack<int> int_stack; // Stack<int, 128>
Stack<std::string, 16> str_stack; // Stack<std::string, 16>
Stack<double, 1024> big_stack;
// Class Template Argument Deduction (CTAD) — C++17:
// Stack s; // Can't deduce T without arguments
// But with a deduction guide:
template<typename T>
Stack(T) -> Stack<T>; // Guide: single value → T inferredNon-Type Template Parameters (NTTPs)
NTTPs let you pass compile-time constants as template arguments — enabling truly zero-overhead fixed-size abstractions:
#include <array>
#include <cstddef>
// Non-type parameter: N is a compile-time constant
template<typename T, size_t N>
class FixedBuffer {
alignas(64) T data_[N]; // N is known at compile time
public:
static constexpr size_t capacity = N;
T& operator[](size_t i) { return data_[i]; }
const T& operator[](size_t i) const { return data_[i]; }
T* data() { return data_; }
size_t size() const { return N; }
};
FixedBuffer<float, 256> audio_buffer; // 1KB on stack — zero allocation
FixedBuffer<uint8_t, 4096> page_buffer; // Exactly one page
// C++20: literal types as NTTPs — even strings!
template<size_t N>
struct StringLiteral {
char value[N];
constexpr StringLiteral(const char (&str)[N]) {
std::copy_n(str, N, value);
}
};
template<StringLiteral Name>
class NamedLogger {
public:
void log(std::string_view msg) {
std::println("[{}] {}", Name.value, msg);
}
};
NamedLogger<"Database"> db_log;
NamedLogger<"Network"> net_log;
// Each has the name baked in at compile time — zero runtime string storageTemplate Specialization: Full and Partial
// Primary template:
template<typename T>
struct TypeInfo {
static const char* name() { return "unknown"; }
static bool is_pointer() { return false; }
};
// Full specialization — for exactly int:
template<>
struct TypeInfo<int> {
static const char* name() { return "int"; }
static bool is_pointer() { return false; }
};
// Full specialization — for exactly double:
template<>
struct TypeInfo<double> {
static const char* name() { return "double"; }
};
// Partial specialization — for any pointer type T*:
template<typename T>
struct TypeInfo<T*> {
static const char* name() { return "pointer"; }
static bool is_pointer() { return true; }
};
// Partial specialization — for std::vector<T>:
template<typename T>
struct TypeInfo<std::vector<T>> {
static const char* name() { return "vector"; }
static bool is_pointer() { return false; }
};
// Usage:
TypeInfo<int>::name(); // "int"
TypeInfo<int*>::name(); // "pointer"
TypeInfo<std::vector<int>>::name(); // "vector"
TypeInfo<float>::name(); // "unknown" (primary template)Variadic Templates and Fold Expressions
#include <iostream>
#include <format>
// Variadic template: accepts any number of type arguments
template<typename... Args>
void print_all(Args&&... args) {
// Fold expression (C++17): applies operator<< to each arg
(std::cout << ... << args) << '\n';
}
print_all(1, " hello ", 3.14, " world"); // "1 hello 3.14 world"
// Sum any number of values of any types:
template<typename... Ts>
auto sum(Ts... values) {
return (... + values); // Unary left fold: ((v1 + v2) + v3) + v4
}
auto total = sum(1, 2, 3, 4, 5); // 15
auto mixed = sum(1, 2.5, 3UL); // double
// Fold expression variants:
// (... op pack) — left fold: ((a op b) op c) op d
// (pack op ...) — right fold: a op (b op (c op d))
// (init op ... op pack) — left fold with initializer
// (pack op ... op init) — right fold with initializer
// Practical: print with separator
template<typename Sep, typename... Args>
void print_sep(Sep sep, Args&&... args) {
bool first = true;
((std::cout << (first ? "" : sep) << args, first = false), ...);
std::cout << '\n';
}
print_sep(", ", 1, "hello", 3.14); // "1, hello, 3.14"
// Recursive variadic (pre-C++17, now prefer fold):
template<typename T>
void print(T val) { std::cout << val << '\n'; }
template<typename T, typename... Rest>
void print(T first, Rest... rest) {
std::cout << first << ' ';
print(rest...); // Recursive call with one fewer argument
}if constexpr: Compile-Time Branching (C++17)
if constexpr evaluates the condition at compile time — the false branch is completely discarded, never compiled:
#include <type_traits>
// Without if constexpr — must use SFINAE (complex):
// With if constexpr — clean and clear:
template<typename T>
std::string to_string(T val) {
if constexpr (std::is_integral_v<T>) {
return std::to_string(val); // Only compiled for integrals
} else if constexpr (std::is_floating_point_v<T>) {
return std::format("{:.6f}", val); // Only compiled for floats
} else if constexpr (requires { val.to_string(); }) {
return val.to_string(); // Only if T has to_string()
} else {
static_assert(false, "T has no string conversion");
}
}
// Recursive variadic with if constexpr:
template<typename T, typename... Rest>
void describe(T first, Rest... rest) {
if constexpr (std::is_integral_v<T>) {
std::cout << "Int: " << first << '\n';
} else {
std::cout << "Other: " << first << '\n';
}
if constexpr (sizeof...(rest) > 0) {
describe(rest...); // Compile-time recursion termination!
}
// No separate base case function needed!
}SFINAE and std::enable_if: Constraining Templates
SFINAE (Substitution Failure Is Not An Error) — if template substitution fails, the overload is silently removed:
#include <type_traits>
// enable_if: only participate in overload resolution for arithmetic types
template<typename T>
std::enable_if_t<std::is_arithmetic_v<T>, T>
safe_divide(T a, T b) {
if (b == T{}) throw std::invalid_argument("Division by zero");
return a / b;
}
// Works for int, float, double — not for string, vector, etc.
safe_divide(10, 3); // OK
safe_divide(10.0, 3.0); // OK
// safe_divide("a", "b"); // SFINAE: removed from overload set, no confusing error
// C++20 preferred: Concepts over SFINAE (cleaner error messages):
template<std::arithmetic T>
T safe_divide_concept(T a, T b) {
if (b == T{}) throw std::invalid_argument("Division by zero");
return a / b;
}Frequently Asked Questions
Why must templates be in header files?
Because the compiler needs the full template definition to instantiate it. When you include a header, the compiler sees the template definition and can generate the specialized code. If the definition is in a .cpp file, other translation units can't instantiate it. Exception: extern template can suppress re-instantiation across TUs, and C++20 modules eliminate the header requirement entirely.
What is "code bloat" and how do I prevent it?
Each unique instantiation generates separate machine code. std::vector<int> and std::vector<double> generate separate versions of all member functions — potentially doubling binary size. Mitigations: (1) Use type-erased base implementations (like std::vector<void*> backing smaller wrappers), (2) Use extern template to instantiate in one TU, (3) Use std::function for callables, (4) C++20 modules reduce repetition.
What is the difference between typename and class in template parameters?
None — template<typename T> and template<class T> are completely interchangeable. typename is preferred in modern code for clarity (the parameter doesn't need to be a class). The only exception: inside a template body, typename is required to disambiguate dependent type names: typename T::value_type.
Key Takeaway
C++ templates are the bridge between high-level expressiveness and bare-metal performance. Every STL container, algorithm, and utility is built on templates. When you understand instantiation, specialization, and SFINAE, you gain the ability to write zero-overhead generic libraries that are impossible in any other mainstream language. C++20 Concepts (next module) make templates readable — the final piece of the template mastery puzzle.
Read next: Concepts & Constraints: Making Templates Readable →
Part of the C++ Mastery Course — 30 modules from modern C++ basics to expert systems engineering.
