Skip to content
C++

Templates Quick Reference

C++ templates cheat sheet — function/class/variable templates, specialization, SFINAE, concepts, fold expressions, CRTP, and template metaprogramming patterns.

Function templates

cpp
// Basic function template
template<typename T>
T max_val(T a, T b) { return a > b ? a : b; }

// Multiple type parameters
template<typename T, typename U>
auto add(T a, U b) -> decltype(a + b) { return a + b; }

// C++14: deduced return type
template<typename T, typename U>
auto multiply(T a, U b) { return a * b; }

// Abbreviated (C++20): auto parameter = hidden template
auto square(auto x) { return x * x; }

// Call: explicit vs deduced
max_val(3, 4);           // T deduced as int
max_val<double>(3, 4);   // T explicitly double

Explicit instantiation and explicit specialization

cpp
// --- Explicit instantiation declaration (in header) ---
// Suppresses implicit instantiation in this TU
extern template int max_val<int>(int, int);

// --- Explicit instantiation definition (in exactly one .cpp) ---
template int max_val<int>(int, int);

// --- Full (explicit) specialization ---
template<>
const char* max_val<const char*>(const char* a, const char* b) {
    return std::strcmp(a, b) > 0 ? a : b;
}

Class templates

cpp
template<typename T, int Capacity = 16>
class Stack {
public:
    void push(const T& val);
    T    pop();
    bool empty() const { return size_ == 0; }
private:
    T   data_[Capacity];
    int size_ = 0;
};

// Member definitions outside class body
template<typename T, int Capacity>
void Stack<T, Capacity>::push(const T& val) {
    data_[size_++] = val;
}

template<typename T, int Capacity>
T Stack<T, Capacity>::pop() {
    return data_[--size_];
}

// Instantiation
Stack<int>        s1;       // Capacity = 16 (default)
Stack<double, 8>  s2;

Non-type template parameters

cpp
// Integer NTTP
template<int N>
struct Array { int data[N]; };

Array<4> a;   // N == 4 at compile time

// C++17: auto NTTP — deduces the type too
template<auto N>
struct Constant {
    static constexpr auto value = N;
};

Constant<42>     c1;   // N is int 42
Constant<'A'>    c2;   // N is char 'A'
Constant<3.14>   c3;   // N is double 3.14  (C++20: floating-point NTTPs allowed)

// Pointer / reference NTTPs
template<const char* Str>
struct Tag {};

Template template parameters

cpp
// A template that takes a container template as a parameter
template<typename T, template<typename, typename> class Container = std::vector>
class Queue {
    Container<T, std::allocator<T>> storage_;
public:
    void enqueue(const T& v) { storage_.push_back(v); }
    T    dequeue()           { T v = storage_.front(); storage_.erase(storage_.begin()); return v; }
};

Queue<int>                        q1;  // backed by std::vector
Queue<int, std::deque>            q2;

Variadic templates and parameter packs

cpp
// Pack expansion with sizeof...
template<typename... Args>
void log(Args&&... args) {
    std::println("arg count: {}", sizeof...(args));
    // Expand each arg:
    (std::print("{} ", args), ...);   // fold expression (C++17)
    std::println();
}

// Recursive (pre-C++17) variadic
template<typename Head, typename... Tail>
void print_all(Head h, Tail... t) {
    std::print("{} ", h);
    if constexpr (sizeof...(t) > 0) print_all(t...);  // C++17 if constexpr
}

// Perfect forwarding of a pack
template<typename... Args>
auto make_vec(Args&&... args) {
    return std::vector{std::forward<Args>(args)...};
}

Fold expressions (C++17)

cpp
// (pack op ...)   — unary right fold
// (... op pack)   — unary left fold
// (pack op ... op init) — binary right fold
// (init op ... op pack) — binary left fold

template<typename... Ts> auto sum(Ts... v)     { return (v + ...);       }
template<typename... Ts> auto all_true(Ts... v){ return (v && ...);      }
template<typename... Ts> auto product(Ts... v) { return (1 * ... * v);   }  // init = 1

// Print all with comma operator fold
template<typename... Ts>
void print_all_fold(Ts&&... args) {
    ((std::print("{} ", args)), ...);
    std::println();
}

// Push multiple items
template<typename Container, typename... Ts>
void push_all(Container& c, Ts&&... vals) {
    (c.push_back(std::forward<Ts>(vals)), ...);
}

SFINAE with std::enable_if_t

cpp
#include <type_traits>

// Enabled only for integral types
template<typename T,
         std::enable_if_t<std::is_integral_v<T>, int> = 0>
T twice(T x) { return x * 2; }

// Enabled only for floating-point types
template<typename T,
         std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
T twice(T x) { return x + x; }

// Via return type
template<typename T>
std::enable_if_t<std::is_pointer_v<T>, T>
advance_ptr(T p, std::ptrdiff_t n) { return p + n; }

void_t detection idiom (C++17)

cpp
#include <type_traits>

// Primary: T has no 'value_type' member
template<typename T, typename = void>
struct has_value_type : std::false_type {};

// Specialization: T has 'value_type' member
template<typename T>
struct has_value_type<T, std::void_t<typename T::value_type>> : std::true_type {};

static_assert( has_value_type<std::vector<int>>::value);  // true
static_assert(!has_value_type<int>::value);               // true

// Check for a callable serialize(T)
template<typename T, typename = void>
struct is_serializable : std::false_type {};

template<typename T>
struct is_serializable<T, std::void_t<decltype(serialize(std::declval<T>()))>>
    : std::true_type {};

Concepts (C++20)

cpp
#include <concepts>

// --- requires clause (on any template) ---
template<typename T>
    requires std::integral<T>
T halve(T x) { return x / 2; }

// --- requires expression — ad-hoc constraint ---
template<typename T>
    requires requires(T a, T b) {
        { a + b } -> std::convertible_to<T>;
        { a.size() } -> std::same_as<std::size_t>;
    }
T add_same(T a, T b) { return a + b; }

// --- Abbreviated function template (C++20) ---
void print_integral(std::integral auto x) { std::println("{}", x); }
auto clamp(std::totally_ordered auto lo,
           std::totally_ordered auto hi,
           std::totally_ordered auto val) {
    return std::clamp(val, lo, hi);
}

// --- Named concept definition ---
template<typename T>
concept Addable = requires(T a, T b) { { a + b } -> std::convertible_to<T>; };

template<Addable T>
T sum3(T a, T b, T c) { return a + b + c; }

// --- Concept + default ---
template<std::regular T = int>
class Box { T value; };

Partial specialization (class templates only)

cpp
// Primary template
template<typename T, typename U>
struct Pair { T first; U second; };

// Partial: both types the same
template<typename T>
struct Pair<T, T> {
    T first, second;
    bool equal() const { return first == second; }
};

// Partial: second type is a pointer
template<typename T, typename U>
struct Pair<T, U*> {
    T first;
    U* second;   // pointer semantics
};

// Partial: specialize on a template template parameter
template<typename T>
struct Pair<std::vector<T>, std::vector<T>> {
    std::vector<T> first, second;
};

Full (explicit) specialization

cpp
// Primary
template<typename T>
struct Formatter { std::string format(const T& v); };

// Full specialization for bool
template<>
struct Formatter<bool> {
    std::string format(bool v) { return v ? "true" : "false"; }
};

// Full specialization for a function template
template<typename T>
void swap_vals(T& a, T& b) { T tmp = a; a = b; b = tmp; }

template<>
void swap_vals<std::string>(std::string& a, std::string& b) { a.swap(b); }

CRTP — Curiously Recurring Template Pattern

cpp
// Base provides behaviour derived from Derived's interface
template<typename Derived>
class Comparable {
public:
    bool operator<=(const Derived& o) const {
        const auto& self = static_cast<const Derived&>(*this);
        return !(o < self);
    }
    bool operator>=(const Derived& o) const {
        const auto& self = static_cast<const Derived&>(*this);
        return !(self < o);
    }
    bool operator>(const Derived& o) const {
        const auto& self = static_cast<const Derived&>(*this);
        return o < self;
    }
    bool operator==(const Derived& o) const {
        const auto& self = static_cast<const Derived&>(*this);
        return !(self < o) && !(o < self);
    }
};

struct Point : Comparable<Point> {
    int x, y;
    bool operator<(const Point& o) const {
        return x == o.x ? y < o.y : x < o.x;
    }
};

// Static polymorphism (zero-overhead, no vtable)
template<typename Derived>
class Logger {
public:
    void log(std::string_view msg) {
        static_cast<Derived*>(this)->write_impl(msg);  // static dispatch
    }
};

struct FileLogger : Logger<FileLogger> {
    void write_impl(std::string_view msg) { /* write to file */ }
};

if constexpr in templates

cpp
#include <type_traits>

template<typename T>
std::string describe(const T& v) {
    if constexpr (std::is_same_v<T, bool>) {
        return v ? "boolean true" : "boolean false";
    } else if constexpr (std::is_integral_v<T>) {
        return "integer: " + std::to_string(v);
    } else if constexpr (std::is_floating_point_v<T>) {
        return "float: " + std::to_string(v);
    } else if constexpr (requires { v.size(); }) {
        return "sized container, size=" + std::to_string(v.size());
    } else {
        return "(unknown)";
    }
}
// Discarded branches are not instantiated — avoids hard errors

decltype, std::declval, trailing return types

cpp
// decltype(expr) — type of an expression without evaluating it
int n = 0;
decltype(n)   x = 5;    // int
decltype(n+1) y = 5;    // int (n+1 not evaluated)
decltype(auto) z = (n); // int& (preserves ref-ness)

// std::declval<T>() — create a T in unevaluated context (no T ctor needed)
template<typename T, typename U>
using AddResult = decltype(std::declval<T>() + std::declval<U>());

// Trailing return type
template<typename T, typename U>
auto add_two(T a, U b) -> decltype(a + b) { return a + b; }

// C++14: just auto, return type deduced
template<typename T, typename U>
auto add_two14(T a, U b) { return a + b; }

// decltype(auto) — deduce including ref qualifiers
template<typename Container>
decltype(auto) first_element(Container& c) { return c[0]; }  // returns T& for vector

Branching strategy comparison

TechniqueC++ versionCompile-timeReadableOverload set
SFINAE + enable_if_tC++11yeslowyes
Tag dispatchC++11yesmediumyes
if constexprC++17yeshighno
Concepts requiresC++20yeshighestyes

When to use each:

  • if constexpr — single function body, different branches per type; simple and readable.
  • SFINAE — still needed in C++11/14 or to constrain overload sets (concepts not available).
  • Tag dispatch — explicit dispatch on std::true_type/std::false_type; useful when the tag logic is shared.
  • Concepts — C++20 preferred; expressive constraints, best error messages, proper overload resolution.

Common type traits quick reference

cpp
// Identity / relationships
std::is_same_v<T, U>            // T and U are the same type (after neither decay)
std::is_base_of_v<Base, Derived>// Derived publicly inherits Base
std::is_convertible_v<From, To> // From is implicitly convertible to To
std::is_constructible_v<T, Args...> // T can be constructed from Args

// Category checks
std::is_integral_v<T>           // bool, char, int, long, ...
std::is_floating_point_v<T>     // float, double, long double
std::is_arithmetic_v<T>         // integral || floating_point
std::is_pointer_v<T>
std::is_reference_v<T>          // lvalue OR rvalue reference
std::is_lvalue_reference_v<T>
std::is_rvalue_reference_v<T>
std::is_array_v<T>
std::is_class_v<T>
std::is_enum_v<T>
std::is_void_v<T>
std::is_function_v<T>

// Modifiers / transformations
std::remove_cv_t<T>             // remove const and volatile
std::remove_reference_t<T>      // remove & or &&
std::remove_cvref_t<T>          // remove cv AND ref (C++20)
std::decay_t<T>                 // array->ptr, func->ptr, remove cvref
std::add_const_t<T>
std::add_lvalue_reference_t<T>
std::add_rvalue_reference_t<T>
std::add_pointer_t<T>           // T -> T*

// Compound
std::conditional_t<Cond, T, U>  // Cond ? T : U
std::enable_if_t<Cond, T>       // T if Cond, else substitution failure
std::void_t<Ts...>              // void if all Ts well-formed (detection)
std::type_identity_t<T>         // just T (prevents deduction)

// Numeric meta
std::common_type_t<Ts...>       // common arithmetic type of Ts
std::make_signed_t<T>
std::make_unsigned_t<T>
std::underlying_type_t<EnumT>   // underlying integer type of an enum

Pitfalls

cpp
// 1. Template definition must be in headers (not .cpp), unless explicitly instantiated
// 2. Dependent names need 'typename' and 'template' keywords
template<typename T>
void foo() {
    typename T::value_type x;           // 'typename' required
    T::template rebind<int>::other y;   // 'template' required
}

// 3. Two-phase lookup: non-dependent names looked up at definition time,
//    dependent names at instantiation time.

// 4. SFINAE only on substitution failure — hard errors (e.g., static_assert)
//    inside a template body still cause a compiler error.

// 5. Concepts give better error messages than SFINAE — prefer them in C++20.

// 6. Partial specialization is for class/variable templates only; you cannot
//    partially specialize a function template (use overloading instead).
Edit on GitHub