C++Generic Programming

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

TT
TopicTrick Team
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

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

cpp
#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 integral

Class Templates: Building Generic Containers

cpp
#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 inferred

Non-Type Template Parameters (NTTPs)

NTTPs let you pass compile-time constants as template arguments — enabling truly zero-overhead fixed-size abstractions:

cpp
#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 storage

Template Specialization: Full and Partial

cpp
// 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

cpp
#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:

cpp
#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:

cpp
#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.