Templates — Advanced Techniques
Advanced C++ templates: SFINAE, concepts, type traits, specialization, CRTP, policy classes, variadic packs, and expression templates.
Advanced Template Techniquessince C++11A 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.
#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 aliasWriting custom type traits
// 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.
// 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
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
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
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
// 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 identityNon-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.
// 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 dispatchCRTP — 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.
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 vtableC++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.
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 APIExpression 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.
// 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.
// 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>) overtypedefinside 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-traceproduces 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.
// 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.
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.
auto expr = a + b; // VecAdd holds refs to a, b
// ... a, b destroyed ...
result = expr; // UB: dangling refs inside exprCRTP 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.
// 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.
// 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.
// 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 independentLambda 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.
// 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.
// 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 = intA practical use-case is heterogeneous algorithms that need to compare or transform elements while retaining their exact types.
// 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,
requiresclauses and expressions - CRTP — dedicated deep-dive including C++23
deducing thisalternatives - 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