Skip to content
C++
Language
since C++11
Expert

Templates — Advanced Techniques

Advanced C++ templates: SFINAE, concepts, type traits, specialization, CRTP, policy classes, variadic packs, and expression templates.

Advanced Template Techniquessince C++11

A family of compile-time mechanisms — SFINAE, partial specialization, variadic packs, CRTP, and expression templates — that enable zero-overhead generic abstractions with type-level computation and static dispatch.

Overview

C++ templates span three distinct eras. Pre-C++11 templates rely on specialization, explicit typename/template disambiguation, and verbose SFINAE patterns. C++11–17 added variadic packs, alias templates, variable templates, fold expressions, and if constexpr. C++20 replaced most SFINAE with concepts — first-class constraints with proper error messages and clean syntax.

Mastering advanced templates requires fluency with all three eras: legacy codebases use SFINAE idioms, modern code uses concepts, and specialization techniques remain universal.


Type traits — compile-time type inspection

The <type_traits> header (C++11) provides introspection predicates. All _v variable template shortcuts require C++17; the ::value form works from C++11.

cpp
#include <type_traits>

// Primary type categories — C++11 traits, C++17 _v shortcuts
static_assert(std::is_integral_v<int>);
static_assert(std::is_floating_point_v<double>);
static_assert(std::is_class_v<std::string>);
static_assert(std::is_pointer_v<int*>);
static_assert(!std::is_same_v<int, long>);          // distinct types, even if same size

// Property queries
static_assert(std::is_nothrow_move_constructible_v<std::vector<int>>);
static_assert(std::is_trivially_copyable_v<int>);

// Type transformations
using T1 = std::remove_cvref_t<const int&>;         // int — C++20
using T2 = std::decay_t<const int&>;                // int — C++11 (also handles array/function decay)
using T3 = std::underlying_type_t<Color>;            // uint8_t, if Color : uint8_t

// Compile-time type selection
using Index = std::conditional_t<(N <= 256), uint8_t,
              std::conditional_t<(N <= 65536), uint16_t, uint32_t>>;  // C++14 alias

Writing custom type traits

cpp
// Detect whether T has a .serialize() const method (C++11 SFINAE idiom)
template<typename T, typename = void>
struct is_serializable : std::false_type {};           // primary: false by default

template<typename T>
struct is_serializable<T,
    std::void_t<decltype(std::declval<const T&>().serialize())>  // C++17 void_t
> : std::true_type {};                                            // fires when expr is valid

template<typename T>
inline constexpr bool is_serializable_v = is_serializable<T>::value;  // C++14

// C++20 equivalent — cleaner, better diagnostics
template<typename T>
concept Serializable = requires(const T& t) { t.serialize(); };

SFINAE — Substitution Failure Is Not An Error

When template argument substitution fails in a function signature (not the body), the overload is silently discarded. std::enable_if exploits this to condition overloads on type properties.

cpp
// C++11: enable_if in return type gates overload selection
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T>
clamp(T val, T lo, T hi) {
    return val < lo ? lo : val > hi ? hi : val;
}

template<typename T>
std::enable_if_t<std::is_floating_point_v<T>, T>
clamp(T val, T lo, T hi) {
    return !(val >= lo) ? lo : !(val <= hi) ? hi : val;  // NaN-safe
}

Enable-if works in three locations: return type, extra defaulted non-type parameter, or trailing typename = std::enable_if_t<...> argument. All three achieve the same overload removal but interact differently when combined.

C++20 — concepts replace SFINAE

cpp
template<std::integral T>
T clamp(T val, T lo, T hi) { return val < lo ? lo : val > hi ? hi : val; }

template<std::floating_point T>
T clamp(T val, T lo, T hi) { return !(val >= lo) ? lo : !(val <= hi) ? hi : val; }

// Composing constraints
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

template<Numeric T>
T abs_val(T x) { return x < T{} ? -x : x; }

Concepts produce errors at the call site naming which constraint failed. SFINAE produces errors deep in instantiation stacks. Prefer concepts for all new C++20+ code.


Template specialization

Full specialization

cpp
template<typename T>
struct Formatter {
    static std::string format(const T& v) {
        std::ostringstream ss; ss << v; return ss.str();
    }
};

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

template<> struct Formatter<std::nullptr_t> {
    static std::string format(std::nullptr_t) { return "null"; }
};

Partial specialization

cpp
template<typename T>
struct Span { T* data; std::size_t size; };

// Specialize for const T — allows implicit narrowing from Span<T>
template<typename T>
struct Span<const T> {
    const T* data; std::size_t size;
    Span(const Span<T>& s) : data(s.data), size(s.size) {}
};

// Specialize for pairs — exposes structured binding-friendly interface
template<typename K, typename V>
struct Span<std::pair<K, V>> {
    std::pair<K, V>* data; std::size_t size;
    K& key(std::size_t i)   { return data[i].first; }
    V& value(std::size_t i) { return data[i].second; }
};

Function templates cannot be partially specialized — use overloading or class template wrappers instead (see Pitfalls).


Variadic templates and fold expressions

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

// C++17: fold expressions collapse packs over an operator
template<typename... Ts>
auto sum(Ts... vs) { return (vs + ...); }               // unary right fold: v0 + (v1 + (v2 + ...))

template<typename... Ts>
bool all_positive(Ts... vs) { return ((vs > 0) && ...); }  // short-circuits

// Perfect forwarding with packs — canonical factory
template<typename T, typename... Args>
std::unique_ptr<T> make(Args&&... args) {
    return std::make_unique<T>(std::forward<Args>(args)...);
}

// Constrained pack (C++20): all args must satisfy a concept
template<std::integral... Ts>
auto product(Ts... vs) { return (vs * ... * 1); }       // binary fold with identity

Non-type template parameters (NTTP)

C++20 extends NTTPs to any structural type (all members public, no user-provided constructors), enabling compile-time string IDs and literal structs as template arguments.

cpp
// C++11: integral NTTP
template<std::size_t N>
struct FixedBuffer { char data[N]; };

// C++20: structural type as NTTP
template<std::size_t N>
struct FixedString {
    char buf[N]{};
    constexpr FixedString(const char (&s)[N]) { std::copy_n(s, N, buf); }
    constexpr bool operator==(const FixedString&) const = default;
};

template<FixedString Tag>               // C++20: string literal as template arg
struct Logger {
    void log(std::string_view msg) {
        std::print("[{}] {}\n", Tag.buf, msg);  // C++23 std::print
    }
};

Logger<"Auth"> auth_log;
Logger<"DB">   db_log;
// Each instantiation is a distinct type — zero-cost tag dispatch

CRTP — Curiously Recurring Template Pattern

CRTP provides static polymorphism: the base class downcasts this to the derived type and calls non-virtual implementation methods. No vtable, full inlining.

cpp
template<typename Derived>
class Comparable {
    const Derived& self() const { return static_cast<const Derived&>(*this); }
public:
    bool operator<=(const Derived& o) const { return !(o < self()); }
    bool operator> (const Derived& o) const { return o < self(); }
    bool operator>=(const Derived& o) const { return !(self() < o); }
    bool operator==(const Derived& o) const { return !(self() < o) && !(o < self()); }
};

struct Version : Comparable<Version> {
    int major, minor, patch;
    bool operator<(const Version& o) const {
        return std::tie(major, minor, patch) < std::tie(o.major, o.minor, o.patch);
    }
};

Version v1{1,2,3}, v2{2,0,0};
bool older = v1 <= v2;  // fully inlined, no vtable

C++23 note: deducing this provides a cleaner alternative for mixins without the recursive template pattern. Prefer it when targeting C++23+.


Policy-based design

Policies inject compile-time-selectable behavior into a host class via template parameters. The host calls into the policy directly; the compiler inlines the call site.

cpp
struct NullMutex {                           // zero-overhead for single-threaded use
    void lock() noexcept {}
    void unlock() noexcept {}
};

struct StdMutex {
    std::mutex m;
    void lock()   { m.lock(); }
    void unlock() { m.unlock(); }
};

template<
    typename T,
    typename MutexPolicy = NullMutex,        // default: no locking overhead
    typename Alloc = std::allocator<T>
>
class BoundedQueue {
    std::vector<T, Alloc> buf_;
    MutexPolicy mtx_;
public:
    void push(T val) {
        mtx_.lock();
        buf_.push_back(std::move(val));
        mtx_.unlock();
    }
};

BoundedQueue<int>            fast_q;   // NullMutex — single-thread path
BoundedQueue<int, StdMutex>  safe_q;  // thread-safe, same API

Expression templates

Expression templates delay evaluation of arithmetic operations by capturing them as a tree of nested template types. At assignment, the entire expression evaluates in one pass — no intermediate allocations or copies.

cpp
// Base expression type — CRTP on the expression itself
template<typename E>
struct VecExpr {
    double operator[](std::size_t i) const { return static_cast<const E&>(*this)[i]; }
    std::size_t size() const { return static_cast<const E&>(*this).size(); }
};

struct Vec : VecExpr<Vec> {
    std::vector<double> data;
    explicit Vec(std::size_t n) : data(n, 0.0) {}
    double operator[](std::size_t i) const { return data[i]; }
    std::size_t size() const { return data.size(); }

    template<typename E>
    Vec& operator=(const VecExpr<E>& expr) {    // assignment from any expression node
        for (std::size_t i = 0; i < data.size(); ++i)
            data[i] = expr[i];                  // single-pass, no temporaries
        return *this;
    }
};

template<typename L, typename R>
struct VecAdd : VecExpr<VecAdd<L, R>> {          // lazy addition node — holds references
    const L& lhs; const R& rhs;
    VecAdd(const L& l, const R& r) : lhs(l), rhs(r) {}
    double operator[](std::size_t i) const { return lhs[i] + rhs[i]; }
    std::size_t size() const { return lhs.size(); }
};

template<typename L, typename R>
VecAdd<L, R> operator+(const VecExpr<L>& l, const VecExpr<R>& r) {
    return {static_cast<const L&>(l), static_cast<const R&>(r)};
}

// result = a + b + c compiles to a single loop — zero heap allocations
Vec a(1000), b(1000), c(1000), result(1000);
result = a + b + c;

Eigen, Blaze, and xtensor use this pattern for matrix arithmetic. The tradeoff: expressions hold references to their operands, so storing an unevaluated expression past operand lifetime is undefined behaviour (see Pitfalls).


Explicit template instantiation

Templates compiled in every TU are a major build-time cost. Explicit instantiation centralises the work.

cpp
// widget.h — suppress instantiation in all includers
extern template class std::vector<Widget>;   // C++11

// widget.cpp — single definition, linked into all TUs
template class std::vector<Widget>;

Pair this with forward declarations for expensive class templates used across many translation units. The -ftime-report (GCC) and --time-trace (Clang) flags identify the worst offenders.


Best Practices

  • Prefer C++20 concepts over SFINAE for any new code — error messages are dramatically better and intent is self-documenting.
  • Constrain templates as tightly as possible. Unconstrained template<typename T> produces cryptic errors deep inside instantiation stacks.
  • Use if constexpr (C++17) to branch on type properties inside template bodies rather than maintaining multiple overloads.
  • Use alias templates (template<typename T> using Ptr = std::shared_ptr<T>) over typedef inside templates — they participate in template argument deduction.
  • Apply explicit instantiation (extern template) to any class template appearing in more than a handful of TUs.
  • Profile compile times before investing in specialization tricks. Clang's --time-trace produces a JSON flamegraph directly showing instantiation cost.

Common Pitfalls

SFINAE only applies in immediate contexts. A substitution failure inside the function body is a hard error, not a silent overload removal.

cpp
// WRONG — static_assert fires regardless of overload selection
template<typename T>
void process(T val) {
    static_assert(std::is_integral_v<T>);  // hard error when T=double
}

// RIGHT — gate at the signature level or use concepts
template<std::integral T>
void process(T val) { /* ... */ }

Partial specialization of function templates is illegal. The workaround is overloading or delegating to a class template.

cpp
template<typename T, typename U> void foo(T, U) {}
// template<typename T> void foo<T, int>(T, int) {}  // error: not allowed
// Correct: add an overload
template<typename T> void foo(T, int) {}

Expression template lifetime. Expression nodes hold references to their operands. Never store an unevaluated expression in auto and use it past the operands' lifetime.

cpp
auto expr = a + b;       // VecAdd holds refs to a, b
// ... a, b destroyed ...
result = expr;           // UB: dangling refs inside expr

CRTP slicing. Passing a CRTP base by value slices away the derived data. Always accept CRTP bases by reference or pointer.


Abbreviated function templates (C++20)

auto parameters are implicit template parameters. A function declared with auto (or a constrained placeholder) in its parameter list is automatically a function template — no explicit template<> header required. The two forms below are exactly equivalent after name lookup.

cpp
// C++20 — abbreviated function template syntax
auto add(auto x, auto y) { return x + y; }

// Equivalent explicit form
template<typename T, typename U>
auto add(T x, U y) { return x + y; }

Abbreviated templates interact cleanly with concepts: placing a concept name in lieu of auto constrains the deduced type just as a constrained template<Concept T> parameter would.

cpp
// C++20 — constrained abbreviated templates
auto scale(std::floating_point auto val, std::floating_point auto factor) {
    return val * factor;
}

// Multiple parameters sharing a concept but allowed to differ in type
auto clamp_to(std::integral auto lo, std::integral auto hi, std::integral auto val) {
    return val < lo ? lo : val > hi ? hi : val;
}

// Combine with trailing return type when the return type must be explicit
std::string stringify(std::formattable<char> auto const& v) {
    return std::format("{}", v);   // C++23 std::format
}

The one limitation relative to explicit template syntax is that abbreviated templates cannot express constraints spanning multiple parameters (e.g., "T and U must be the same type"). For those cases, an explicit template<typename T> header with a requires clause remains necessary.

cpp
// C++20 — requires clause enforces cross-parameter relationship
template<typename T>
requires std::same_as<T, std::remove_cvref_t<T>>
T identity(T val) { return val; }

// Abbreviated syntax cannot express this — no shared name across params
// auto identity(auto val) { ... }  // T1 and T2 would be independent

Lambda templates (C++20)

Generic lambdas in C++14 deduce types but cannot name them. When a generic lambda body needs to operate on the deduced type itself — to declare a local variable of that type, call a static method, or forward with perfect precision — C++14 auto parameters fall short. C++20 lambda templates solve this by letting the capture list carry an explicit template parameter list.

cpp
// C++20 — explicit template parameter list on a lambda
auto typed_print = []<typename T>(T val) {
    std::cout << typeid(T).name() << ": " << val << '\n';
};

typed_print(42);        // T = int
typed_print(3.14);      // T = double
typed_print("hello");   // T = const char*

Lambda templates unlock variadic generic lambdas with pack expansion, which C++14 generic lambdas cannot express.

cpp
// C++20 — variadic lambda template for heterogeneous argument forwarding
auto forward_all = []<typename... Ts>(Ts&&... args) {
    (process(std::forward<Ts>(args)), ...);  // C++17 fold expression
};

// C++20 — constrained lambda template
auto sum_integrals = []<std::integral T>(std::initializer_list<T> vals) -> T {
    T total{};
    for (T v : vals) total += v;
    return total;
};

int s = sum_integrals({1, 2, 3, 4, 5});    // T = int

A practical use-case is heterogeneous algorithms that need to compare or transform elements while retaining their exact types.

cpp
// C++20 — lambda template for transparent max over any comparable pair
auto typed_max = []<typename T>(const T& a, const T& b) -> const T& {
    return a < b ? b : a;
};

auto& m = typed_max(std::string{"apple"}, std::string{"banana"});  // no type erasure

// Equivalent explicit template function — same semantics, more boilerplate
template<typename T>
const T& typed_max_fn(const T& a, const T& b) { return a < b ? b : a; }

Lambda templates are particularly valuable in generic library internals where a helper needs to be locally scoped (to avoid polluting a header's namespace) but must still operate on named types.


See Also

  • Templates — Basics — instantiation mechanics, argument deduction, function templates
  • Concepts — C++20 constraint syntax, requires clauses and expressions
  • CRTP — dedicated deep-dive including C++23 deducing this alternatives
  • if constexpr — compile-time branching inside template bodies
  • Fold Expressions — C++17 pack reduction syntax reference
  • Deducing This — C++23 explicit object parameter, replaces many CRTP uses