← Back to blog

C++ Templates and SFINAE: Writing Generic Code That Just Works

C++ templates let you write code once and use it with many types. The hard part is making that code behave well across a wide range of inputs:

  • Prefer the “right” implementation when a type has certain capabilities.
  • Produce clear compile-time failures when requirements aren’t met.
  • Avoid forcing users into awkward wrapper types.

SFINAE—Substitution Failure Is Not An Error—is a classic tool for this. It allows the compiler to discard template candidates when substituting template arguments fails, instead of treating that failure as a hard error.

Audience / prerequisites (and language level)

This post assumes you’re comfortable with:

  • Function overload resolution (how overload sets are formed and selected)
  • Type traits (std::is_integral_v, etc.)
  • decltype, std::declval, and reading template error messages

All SFINAE examples are C++17-friendly. The final section shows the C++20 concepts equivalent.

What you’ll be able to implement by the end

We’ll build a small toolkit of patterns you can reuse:

  • std::enable_if overload gating (and where to put it)
  • The detection idiom (std::void_t + decltype) for “is this expression well-formed?”
  • Choosing between SFINAE overloads, tag dispatch, and if constexpr
  • A practical multi-tier fallback function (to_string_like)
  • A direct translation of the same ideas into concepts (C++20)

What SFINAE actually means (and what it doesn’t)

When the compiler considers a function template overload set, it tries to substitute template arguments into parts of the function template’s signature (and a few related places). If that substitution fails, the compiler doesn’t immediately error. Instead, it removes that overload from consideration.

Two key distinctions make SFINAE less mysterious:

  • Substitution failure (SFINAE): failure while substituting into the template’s immediate context → candidate is discarded.
  • Instantiation error (hard error): failure that happens when the compiler instantiates/compiles the selected template body (or in a non-SFINAE context) → compilation fails.

“Immediate context” in one snippet

The phrase immediate context roughly means “the part of the template the compiler must form to decide whether the overload is viable” (e.g., return type, parameter types, default template arguments used in the signature).

In contrast, the function body is not part of the immediate context.

#include <type_traits>
#include <utility>

// SFINAE context: return type uses decltype(...)
template <class T>
auto ok_if_has_size(const T& t) -> decltype(t.size(), void()) {
    // body can be empty; viability was decided by substitution
}

// Not SFINAE: the body is compiled only after overload selection
template <class T>
void hard_error_in_body(const T& t) {
    t.size(); // if T has no size(), this is a hard compile error
}

If T has no size(), ok_if_has_size is simply removed from the overload set; hard_error_in_body compiles until it’s instantiated, then fails loudly.

Caption: Overload set before substitution vs after substitution—SFINAE removes candidates whose signatures can’t be formed.

SFINAE is not only for function overloads

SFINAE most commonly shows up in function template overload resolution, but the same “substitution failure discards a candidate” idea also appears in partial specialization matching for class templates (and variable templates). We won’t dive deep into partial specializations here, but it’s useful to know SFINAE isn’t “only an overload trick.”

The simplest form: std::enable_if

std::enable_if is a type-level switch:

  • If a boolean condition is true, it provides a nested type (or std::enable_if_t<...>).
  • If false, there is no type, and substitution fails.

A common pattern is to enable a function only for integral types:

#include <type_traits>
#include <iostream>

template <class T,
          std::enable_if_t<std::is_integral_v<T>, int> = 0>
void print_kind(const T&) {
    std::cout << "integral\n";
}

template <class T,
          std::enable_if_t<!std::is_integral_v<T>, int> = 0>
void print_kind(const T&) {
    std::cout << "non-integral\n";
}

int main() {
    print_kind(42);      // integral
    print_kind(3.14);    // non-integral
}

Notes:

  • The int = 0 is a dummy non-type template parameter used to carry the enable_if.
  • These two overloads are mutually exclusive because the conditions are exact negations. In real code, if conditions can overlap, you can get ambiguity. Avoid overlap by making constraints strictly ordered (or by adding a “more constrained” overload + a broad fallback).

Where to put enable_if (and why return-type SFINAE can surprise you)

You’ll see three common placements:

  1. Template parameter (as above): predictable and common.
  2. Dummy function parameter:
    template <class T>
    void f(T, std::enable_if_t<std::is_integral_v<T>, int> = 0);
    
    This can be nicer when you want to avoid interactions with explicit template argument specification (and sometimes yields clearer diagnostics).
  3. Return type:
    template <class T>
    std::enable_if_t<std::is_integral_v<T>, void> f(T);
    

Return-type SFINAE often hurts readability, and it can behave differently than people expect because the return type doesn’t participate in overload resolution in the same “direct” way as parameter types. It can also worsen diagnostics when combined with other templates.

Detection: checking whether an expression is well-formed

Most real-world SFINAE use is about checking whether some expression is valid for a type.

Example: “Does T support streaming to std::ostream?”

The detection idiom (C++17-friendly)

You can build a trait that evaluates to true if an expression is well-formed:

#include <type_traits>
#include <utility>
#include <ostream>

template <class, class = void>
struct is_ostreamable : std::false_type {};

template <class T>
struct is_ostreamable<T, std::void_t<
    decltype(std::declval<std::ostream&>() << std::declval<const T&>())
>> : std::true_type {};

template <class T>
inline constexpr bool is_ostreamable_v = is_ostreamable<T>::value;

Two important clarifications:

  • This checks well-formedness, not semantics. It doesn’t prove the output is meaningful, stable, or cheap—only that the expression compiles.
  • operator<< can be found via ADL (argument-dependent lookup). That’s usually what you want, but it can also pick up surprising overloads.

Common refinement: require the return type

Many codebases strengthen “ostreamable” to require that the expression returns std::ostream&:

template <class, class = void>
struct is_ostreamable_strict : std::false_type {};

template <class T>
struct is_ostreamable_strict<T, std::void_t<
    decltype(std::declval<std::ostream&>() << std::declval<const T&>())
>> : std::bool_constant<
    std::is_same_v<
        decltype(std::declval<std::ostream&>() << std::declval<const T&>()),
        std::ostream&
    >
> {};

Whether you need this depends on how strict you want your constraint to be.

Using the trait to enable overloads

Now you can conditionally enable overloads:

#include <iostream>
#include <string>

template <class T, std::enable_if_t<is_ostreamable_v<T>, int> = 0>
void print(const T& value) {
    std::cout << value;
}

template <class T, std::enable_if_t<!is_ostreamable_v<T>, int> = 0>
void print(const T&) {
    std::cout << "<unprintable>";
}

This is a good example of “generic code that just works”: printable types print nicely, and non-printable types still compile and degrade gracefully.

Caption: Detection idiom flow—try to form an expression in decltype(...); if it fails, the specialization is discarded and the fallback trait wins.

Tag dispatch vs SFINAE overloads (and if constexpr as a middle ground)

Sometimes SFINAE is overkill. If you already have a trait you trust, tag dispatch can be simpler.

#include <type_traits>
#include <iostream>

template <class T>
void process_impl(const T&, std::true_type) {
    std::cout << "fast path for trivially copyable\n";
}

template <class T>
void process_impl(const T&, std::false_type) {
    std::cout << "safe path\n";
}

template <class T>
void process(const T& x) {
    process_impl(x, std::is_trivially_copyable<T>{});
}

What’s really happening:

  • SFINAE overloads: remove invalid candidates from overload resolution.
  • Tag dispatch: keeps a single public API and selects an implementation by passing a tag type.

Also note the subtlety: tag dispatch does not “remove candidates.” Both process_impl overloads are declared and available; the call chooses one based on the tag type.

When tag dispatch can still bite you

Tag dispatch is usually safe, but you can still get hard errors if the “unused” branch becomes instantiated indirectly (e.g., you reference both branches in a dependent context, or you put invalid code in a context that gets instantiated regardless). A modern alternative is often clearer.

if constexpr (C++17) as a practical alternative

If your choice is driven by a boolean trait, if constexpr avoids overload sets entirely:

template <class T>
void process(const T& x) {
    if constexpr (std::is_trivially_copyable_v<T>) {
        // fast path
    } else {
        // safe path
    }
}

This is often the first thing to reach for in C++17 when you don’t need overload selection.

Avoiding common SFINAE footguns

SFINAE is powerful, but it’s easy to make templates fragile or produce awful error messages.

1) Prefer SFINAE in template parameters (often), but know the alternatives

Template-parameter SFINAE is predictable, but it can interact awkwardly with explicit template argument specification and sometimes leads to long diagnostics.

A common alternative is a trailing dummy function parameter:

template <class T>
void f(const T&, std::enable_if_t<std::is_integral_v<T>, int> = 0);

This can be easier for compilers to explain, and it avoids making the template parameter list itself “mysterious.”

2) Constrain on capabilities, not ad-hoc type lists

Instead of listing types (e.g., allow int, long, short…), test the operation you need:

  • “Can I call begin(t) and end(t)?”
  • “Can I do t.size()?”
  • “Can I stream t?”

This aligns your constraints with your actual requirements.

3) Beware of non-immediate contexts (actionable examples)

SFINAE only applies in certain contexts. Here are common places where an invalid expression becomes a hard error:

  • Inside the function body (as shown earlier)
  • Default arguments
  • Base classes / member declarations

Example: default argument is not a SFINAE escape hatch:

template <class T>
void g(T, int = sizeof(typename T::no_such_type)) {
    // If T::no_such_type doesn't exist, this is typically a hard error
    // when g<T> is instantiated.
}

Rule of thumb: validate the expression in the constraint (immediate context), and keep the body “boring.”

template <class T, std::enable_if_t<is_ostreamable_v<T>, int> = 0>
void print(const T& value) {
    // safe: the expression was validated by the constraint
    std::cout << value;
}

Diagnostics: graceful fallback vs static_assert

A fallback like "<unprintable>" can be great for logging utilities, debugging helpers, or “best-effort” formatting.

For APIs where a fallback might hide a bug, prefer failing loudly with a clear message:

template <class T>
void serialize(const T&)
{
    static_assert(is_ostreamable_v<T>,
                  "serialize(T) requires: std::ostream& << const T&");
}

You can combine approaches too: provide a fallback for “debug print,” but require strict constraints for “wire format serialization.”

A practical example: to_string_like that adapts

Let’s build a utility that tries, in order:

  1. Return strings directly (std::string, std::string_view, C strings).
  2. Use std::to_string for arithmetic types.
  3. Else, stream to std::ostringstream if streamable.
  4. Else, return a fallback.

Two caveats worth stating up front:

  • std::to_string only supports a specific set of arithmetic types. It’s not a general “stringify anything.”
  • If your type is implicitly convertible to an arithmetic type, std::to_string may be selected unexpectedly. If that matters, tighten constraints (e.g., std::is_arithmetic_v<T> and/or disallow user-defined conversions).
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <sstream>
#include <ostream>

// Detect std::to_string(t)
template <class, class = void>
struct has_std_to_string : std::false_type {};

template <class T>
struct has_std_to_string<T, std::void_t<
    decltype(std::to_string(std::declval<T>()))
>> : std::true_type {};

template <class T>
inline constexpr bool has_std_to_string_v = has_std_to_string<T>::value;

// Reuse is_ostreamable_v from earlier

// 1) string-like fast paths
inline std::string to_string_like(std::string s) { return s; }
inline std::string to_string_like(std::string_view sv) { return std::string(sv); }
inline std::string to_string_like(const char* s) { return s ? std::string(s) : std::string("<null>"); }

// 2) std::to_string (often best restricted to arithmetic)
template <class T,
          std::enable_if_t<std::is_arithmetic_v<T> && has_std_to_string_v<T>, int> = 0>
std::string to_string_like(const T& v) {
    return std::to_string(v);
}

// 3) stream fallback
template <class T,
          std::enable_if_t<!(std::is_arithmetic_v<T> && has_std_to_string_v<T>) && is_ostreamable_v<T>, int> = 0>
std::string to_string_like(const T& v) {
    std::ostringstream oss;
    oss << v;
    return oss.str();
}

// 4) final fallback
template <class T,
          std::enable_if_t<!(std::is_arithmetic_v<T> && has_std_to_string_v<T>) && !is_ostreamable_v<T>, int> = 0>
std::string to_string_like(const T&) {
    return "<unstringifiable>";
}

Tradeoffs to be aware of:

  • std::ostringstream can be relatively heavy and is locale-aware; formatting may differ from std::to_string.
  • In C++20, std::format (or the library) is often a better formatting tool when you control the format string.

Caption: Multi-tier selection—string-like → arithmetic (to_string) → streamable → fallback. Each tier removes/accepts candidates via constraints.

Modern C++ note: concepts can replace most SFINAE

If you can use C++20, concepts are usually a better tool:

  • Constraints are readable.
  • Error messages are far clearer.
  • You can express requirements directly.

For example, the ostreamable constraint becomes a named concept:

#include <concepts>
#include <ostream>

template <class T>
concept Ostreamable = requires(std::ostream& os, const T& v) {
    { os << v } -> std::same_as<std::ostream&>;
};

And you can mirror the earlier overload pair directly:

#include <iostream>

template <Ostreamable T>
void print(const T& v) {
    std::cout << v;
}

template <class T>
void print(const T&) {
    std::cout << "<unprintable>";
}

For type categories, prefer standard concepts where they exist (e.g., std::integral instead of std::is_integral_v).

Even if you adopt concepts, understanding SFINAE remains valuable because:

  • Many libraries still use it internally.
  • You’ll read it in existing codebases.
  • Pre-C++20 targets still rely on it.

Checklist: writing generic code that “just works”

When designing a template API:

  • Constrain on capabilities (valid expressions), not specific types.
  • Provide a fallback overload when it makes sense.
  • Keep constraints close to the function signature.
  • Decide deliberately between graceful fallback vs static_assert diagnostics.
  • If you have C++20, prefer concepts for clarity.

Further pitfalls / next steps

If you want to go deeper, watch for these recurring issues:

  • Ambiguous overload sets when constraints overlap (add strict ordering or a single constrained overload + one unconstrained fallback).
  • Constraints that are too broad (e.g., implicit conversions causing “wrong tier” selection).
  • ADL surprises (especially around operator<<, begin/end, and customization points).
  • When available, test intent with requires/concepts first—they’re often the clearest way to document what your template expects.