Skip to content
C++
Language
since C++98
Advanced

Templates

Compile-time code parameterization in C++. Function, class, variable, and alias templates; specialization; variadic packs; SFINAE vs C++20 concepts.

Templatessince C++98

Templates are a compile-time parameterization mechanism that instructs the compiler to generate one concrete definition per unique combination of template arguments, enabling type-safe generic code with zero runtime overhead.

Overview

The compiler does not compile a template β€” it compiles its instantiations. When you write std::vector<int>, the compiler synthesizes a class named (conceptually) vector<int> from the template definition. This instantiation happens at compile time and appears in the translation unit that triggers it, unless you use explicit instantiation.

Four kinds of templates exist:

  • Function templates β€” parameterize a function over types or values (C++98)
  • Class templates β€” parameterize a class or struct (C++98)
  • Variable templates β€” parameterize a variable (C++14)
  • Alias templates β€” parameterize a type alias (C++11)

C++20 adds abbreviated function templates (auto parameters) and constraints (requires), which replace most SFINAE idioms.

Syntax

Template parameter kinds

cpp
// Type parameter (typename and class are interchangeable here)
template<typename T>        void f(T);
template<class   T>        void f(T);  // identical

// Non-type parameter: any structural type
template<int N>            struct Fixed;          // C++98
template<std::size_t N>    struct Fixed;          // C++98
template<auto N>           struct Fixed;          // C++17: deduced NTTP
template<std::string_view> struct Fixed;          // C++20: class-type NTTP

// Template-template parameter
template<template<typename> class Container>
void adapt(Container<int>& c);

// Parameter packs (variadic)
template<typename... Ts>   void f(Ts...);          // C++11
template<int... Ns>        struct Seq;             // C++11

// Default template arguments
template<typename T, typename Alloc = std::allocator<T>>
class MyVec;

Variable templates (C++14)

cpp
template<typename T>
constexpr T pi = T(3.14159265358979323846L);

float  a = pi<float>;   // 3.14159f
double b = pi<double>;  // full precision

Alias templates (C++11)

cpp
template<typename T>
using Vec = std::vector<T, std::pmr::polymorphic_allocator<T>>;

Vec<int> v;  // std::vector<int, pmr::polymorphic_allocator<int>>

Examples

CTAD and deduction guides (C++17)

Class Template Argument Deduction eliminates explicit type arguments when the compiler can infer them:

cpp
std::pair  p{1, 3.14};          // C++17: pair<int, double>
std::array arr{1, 2, 3};        // C++17: array<int, 3>
std::mutex m;
std::lock_guard lk{m};          // C++17: lock_guard<std::mutex>

For custom templates, provide deduction guides when the constructor arguments do not directly match the template parameters:

cpp
template<typename T>
struct Span {
    T* ptr;
    std::size_t len;
};

// Deduction guide: Span(T*, size_t) -> Span<T>
template<typename T>
Span(T*, std::size_t) -> Span<T>;

int arr[4]{};
Span s{arr, 4u};  // Span<int>

Variadic templates and fold expressions (C++11/17)

cpp
// Recursive sum β€” C++11 approach
template<typename T>
T sum(T t) { return t; }

template<typename T, typename... Rest>
T sum(T first, Rest... rest) { return first + sum(rest...); }

// Fold expression β€” C++17, far simpler
template<typename... Args>
auto sum_fold(Args... args) {
    return (args + ...);  // right fold: a + (b + (c + ...))
}

// Practical: forward to multiple sinks
template<typename T, typename... Sinks>
void broadcast(T&& value, Sinks&... sinks) {
    (sinks.write(value), ...);  // C++17 comma-fold
}

if constexpr for type dispatch (C++17)

cpp
template<typename T>
std::string to_hex(T val) {
    if constexpr (std::is_integral_v<T>) {        // C++17
        std::ostringstream oss;
        oss << std::hex << val;
        return oss.str();
    } else if constexpr (std::is_floating_point_v<T>) {
        // floats have no meaningful hex representation here
        return std::to_string(val);
    } else {
        static_assert(std::is_integral_v<T>,       // always fires for other types
                      "to_hex requires an integral type");
    }
}

The discarded branch is parsed but not instantiated, which means dependent expressions that would be ill-formed are safely ignored β€” unlike runtime if.

Constraints with requires (C++20)

cpp
#include <concepts>

// Constrained with concept shorthand
template<std::integral T>
T abs_val(T x) { return x < 0 ? -x : x; }

// Explicit requires clause β€” useful for compound constraints
template<typename T>
    requires std::is_arithmetic_v<T> && (!std::is_same_v<T, bool>)
T clamp(T v, T lo, T hi) {
    return v < lo ? lo : v > hi ? hi : v;
}

// Abbreviated template (auto parameter) β€” C++20
void print(std::ranges::range auto const& r) {   // C++20
    for (auto const& e : r) std::cout << e << ' ';
}

// Ad-hoc concept in requires expression
template<typename T>
    requires requires(T a, T b) { a + b; a < b; }
T bounded_add(T a, T b, T max_val) {
    return (a + b < max_val) ? a + b : max_val;
}

Explicit instantiation (reduce translation-unit bloat)

cpp
// widget.h β€” declaration only
extern template class std::vector<Widget>;   // C++11: suppress implicit instantiation

// widget.cpp β€” one explicit instantiation for the whole program
template class std::vector<Widget>;

This is useful for heavy types that appear in many translation units. Without it, every TU instantiates the template and the linker collapses duplicates β€” wasting compile time. With explicit instantiation, only one TU does the work.

Template specialization

Full specialization replaces the entire template for a specific argument set:

cpp
template<typename T>
struct Serialize {
    static std::string encode(const T& v) { /* generic */ }
};

template<>  // full specialization
struct Serialize<float> {
    static std::string encode(float v) {
        return std::to_string(v);  // custom path for float
    }
};

Partial specialization is available for class and variable templates (not function templates):

cpp
// Generic: not a pointer
template<typename T>
constexpr bool is_pointer_v = false;

// Partial: any pointer-to-T
template<typename T>
constexpr bool is_pointer_v<T*> = true;    // C++14 variable template

// Partial: pointer-to-const-T
template<typename T>
constexpr bool is_pointer_v<const T*> = true;

Best Practices

Constrain early. An unconstrained template<typename T> accepts everything and produces incomprehensible errors when T lacks an expected operation. Use concepts (C++20) or static_assert (C++11) at the function body top if C++20 is unavailable.

Prefer typename to class in template parameter lists for clarity β€” class is identical but implies "only class types" to readers, which is wrong.

Use requires over enable_if wherever C++20 is available. enable_if_t pollutes the signature, produces unreadable errors, and introduces subtle overload-resolution pitfalls. A requires clause is first-class in overload resolution.

Separate interface from implementation when using extern template. Place the explicit instantiation declaration (extern template) in the header and the definition in exactly one .cpp. This pattern is used by the standard library itself for common specializations.

Prefer fold expressions over recursive variadic helpers (C++17). Recursion creates O(N) instantiations; a fold expression is a single instantiation.

Non-type template parameters: prefer auto N (C++17) over repeating the type. Lets the caller pass std::size_t, int, ptrdiff_t, etc., and the type is deduced correctly.

Common Pitfalls

Dependent name lookup requires typename and template

cpp
template<typename T>
void process(T& obj) {
    // Without typename: compiler assumes T::value_type is a value, not a type
    typename T::value_type tmp;           // correct

    // Without template keyword: '<' is parsed as less-than
    obj.template apply<int>(tmp);         // correct β€” method is a template
}

Names are looked up in two phases: non-dependent names at the point of template definition, dependent names at the point of instantiation. Forgetting typename for dependent type names is the single most common template diagnostic.

Overload resolution: non-template beats template

cpp
template<typename T> void f(T);    // (1)
void f(int);                       // (2) β€” non-template

f(0);    // calls (2) β€” exact match on non-template preferred
f(0L);   // calls (1) with T=long β€” no non-template exact match
f<int>(0); // calls (1) explicitly

This applies to class member functions too: a non-template operator== beats a templated one even when both match.

static_assert(false, ...) in an if-constexpr else branch

cpp
// WRONG: the static_assert fires even if the branch is never taken
template<typename T>
void f(T) {
    if constexpr (std::is_integral_v<T>) { /* ... */ }
    else { static_assert(false, "unsupported"); }  // always fails
}

// CORRECT: make the assertion dependent on T
template<typename T>
void f(T) {
    if constexpr (std::is_integral_v<T>) { /* ... */ }
    else { static_assert(!sizeof(T), "unsupported"); }  // dependent β€” fires only when instantiated
}

Template argument deduction does not consider implicit conversions

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

max_val(1, 2);          // OK: T=int
max_val(1, 2.0);        // Error: T deduced as both int and double
max_val<double>(1, 2.0);// OK: explicit argument, 1 converts to double

Deduction matches types exactly; implicit conversions happen only after deduction succeeds (for explicit arguments or after deduction succeeds uniformly).

Greedy function template vs. better non-template overload

cpp
template<typename T>
bool equals(const T& a, const T& b) { return a == b; }

// Caller expects pointer equality:
int* p = new int(5);
int* q = p;
equals(p, q);   // calls template, compares addresses β€” correct
equals("abc", "abc");  // compares char const* addresses β€” may return false!

Add a specialization or overload for const char* to handle C-string semantics explicitly.

Non-Type Template Parameters

NTTPs let you bake compile-time values directly into a type. Instead of passing a buffer size at runtime, you can make it part of the type itself β€” enabling the compiler to size stack arrays, unroll loops, and optimize across TU boundaries with no overhead.

cpp
// C++98
template<typename T, std::size_t N>
struct FixedArray {
    T data[N];
    static constexpr std::size_t size() { return N; }
};

FixedArray<int, 8>  small;   // N=8 baked into the type
FixedArray<int, 64> large;   // completely different type

C++17 introduced auto NTTPs so the parameter's type is deduced from the argument, removing the need to repeat it in every usage.

cpp
// C++17
template<auto N>
struct Repeat {
    static constexpr auto value = N;
};

Repeat<42>    a;   // N deduced as int
Repeat<42ULL> b;   // N deduced as unsigned long long
Repeat<'x'>   c;   // N deduced as char

C++20 expanded the set of valid NTTP types to include floats and any structural user-defined type β€” a literal class whose every non-static data member is public and has a structural type itself.

cpp
// C++20
template<double Tolerance>
bool approx_equal(double a, double b) {
    return std::abs(a - b) < Tolerance;
}

bool ok = approx_equal<1e-6>(3.14159, 3.14158);  // C++20: float NTTP

// C++20 structural class NTTP
struct Flags { bool debug; bool verbose; };

template<Flags F>
void run() {
    if constexpr (F.debug)   std::clog << "debug mode\n";
    if constexpr (F.verbose) std::clog << "verbose mode\n";
}

run<Flags{.debug=true, .verbose=false}>();  // C++20

The structural-type rule makes it practical to embed small configuration objects directly into template arguments, giving each configuration its own concrete type (and its own optimized code path) with no heap allocation.

Template Template Parameters

A template template parameter accepts a template as an argument, not a type. This lets you write generic adapters that are parameterized over a container family β€” not a single concrete container β€” while preserving the container's element type.

cpp
// C++98 β€” template template parameter: Container must take one type argument
template<typename T, template<typename> class Container>
class Stack {
    Container<T> storage_;
public:
    void push(const T& v) { storage_.push_back(v); }
    void pop()            { storage_.pop_back(); }
    T&   top()            { return storage_.back(); }
};

Stack<int, std::vector> vs;   // Container=std::vector, T=int
Stack<int, std::deque>  ds;   // Container=std::deque,  T=int

Standard containers take more than one type parameter (allocator, etc.), so the single-argument form above will not match them without C++17's variadic template template parameter syntax.

cpp
// C++17 β€” variadic template template parameter matches any number of params
template<typename T, template<typename...> class Container>
class FlexStack {
    Container<T> storage_;
public:
    void push(const T& v) { storage_.push_back(v); }
    T    pop()            { T v = storage_.back(); storage_.pop_back(); return v; }
};

FlexStack<int, std::vector> a;   // C++17: matches vector<int, allocator<int>>
FlexStack<int, std::list>   b;   // C++17: matches list<int, allocator<int>>

Template template parameters are rarely the right first tool, but they shine when you need to forward a container policy through multiple layers of generic code without hard-coding the element type.

Variadic Templates

Variadic templates accept an arbitrary number of template arguments of any types. They underpin std::tuple, std::variant, perfect-forwarding factories, and virtually every generic logging or serialization helper written since C++11.

cpp
// C++11 β€” pack expansion into function argument list
template<typename... Args>
void print_all(Args&&... args) {
    // Expands to: print(args1), print(args2), ...
    (print(std::forward<Args>(args)), ...);  // C++17 comma-fold (see below)
}

sizeof... queries the number of elements in a pack without expanding it.

cpp
// C++11
template<typename... Ts>
constexpr std::size_t count() { return sizeof...(Ts); }

static_assert(count<int, double, char>() == 3);

The classic C++11 approach to operating over a pack was mutual recursion. C++17 fold expressions replace it with a single instantiation.

cpp
// C++11 recursive sum (creates O(N) instantiations)
template<typename T>
T sum_impl(T v) { return v; }

template<typename T, typename... Rest>
T sum_impl(T first, Rest... rest) {
    return first + sum_impl(rest...);
}

// C++17 fold expressions (one instantiation, four fold operators)
template<typename... Args>
auto sum(Args... args) {
    return (args + ...);          // unary right fold:  a+(b+(c+0))
}

template<typename... Args>
auto product(Args... args) {
    return (... * args);          // unary left fold:   ((a*b)*c)
}

template<typename... Args>
bool all_positive(Args... args) {
    return ((args > 0) && ...);   // short-circuits on first false
}

template<typename T, typename... Args>
auto sum_with_init(T init, Args... args) {
    return (init + ... + args);   // binary left fold: ((init+a)+b)
}

Pack expansion is valid in most contexts: base-class lists, initializer lists, sizeof..., function argument lists, and template argument lists.

cpp
// C++11 β€” expand pack into base-class list (mixin pattern)
template<typename... Mixins>
struct Combined : Mixins... {
    using Mixins::operator()...;   // C++17: bring all call operators into scope
};

// C++11 β€” perfect-forwarding factory
template<typename T, typename... Args>
std::unique_ptr<T> make(Args&&... args) {
    return std::make_unique<T>(std::forward<Args>(args)...);
}

Explicit Instantiation

Explicit instantiation tells the compiler to emit a concrete instantiation in exactly one translation unit, preventing every other TU from doing it redundantly. Large codebases with widely-used templates (containers, serializers, math types) can cut link times and object sizes dramatically by centralizing instantiations.

The mechanism uses two complementary declarations: a definition in one .cpp and a suppression declaration (extern template) in the shared header.

cpp
// C++11 β€” mytype.h (included by everyone)
#pragma once
#include <vector>
#include "mytype.hpp"  // full template definition must be visible here

// Suppress implicit instantiation in every including TU:
extern template class std::vector<MyType>;          // C++11
extern template void serialize(const MyType&, Stream&); // function template
cpp
// C++11 β€” mytype.cpp (compiled once)
#include "mytype.h"

// Explicit instantiation definition β€” the compiler emits the full code here:
template class std::vector<MyType>;
template void serialize(const MyType&, Stream&);

Without extern template, each TU that includes mytype.h and uses std::vector<MyType> instantiates every member function of that specialization. The linker deduplicates them (via COMDAT/weak symbols), but the compiler still does the work in every TU. With extern template the compiler skips instantiation at each include site and trusts the linker to find the one definition.

Explicit instantiation is also the standard way to expose template instantiations from a shared library without making the full template definition part of the public API.

cpp
// C++11 β€” mathlib.h (shipped with the .so, no template body exposed)
template<typename T> T lerp(T a, T b, T t);  // declaration only

extern template float  lerp(float,  float,  float);
extern template double lerp(double, double, double);

// mathlib.cpp (compiled into the .so)
template<typename T>
T lerp(T a, T b, T t) { return a + t * (b - a); }

template float  lerp(float,  float,  float);   // explicit instantiation
template double lerp(double, double, double);

Callers link against the .so and get lerp<float> and lerp<double> without the template body being visible in their TU. Only those two specializations are available; attempting to call lerp<int> produces a linker error, not a compiler error β€” something to document explicitly when shipping library code.

See Also

  • reference/language/concepts β€” define reusable named constraints for cleaner template APIs
  • reference/language/fold-expressions β€” C++17 syntax for operating over parameter packs without recursion
  • reference/language/if-constexpr β€” compile-time branching within template bodies
  • reference/language/ctad β€” Class Template Argument Deduction rules and deduction guides in depth
  • reference/language/sfinae-concepts β€” pre-C++20 technique for conditional template enablement
  • reference/language/crtp β€” Curiously Recurring Template Pattern for static polymorphism