Skip to main content

SFINAE and enable_if

SFINAE (Substitution Failure Is Not An Error) is a fundamental C++ template mechanism that enables conditional template compilation. std::enable_if is the primary tool for applying SFINAE in practice.

Conditional Template Compilation

SFINAE: When template substitution fails, remove template from consideration instead of erroring
enable_if: Enable/disable templates based on compile-time conditions

In C++20, prefer concepts

SFINAE works but is hard to read and produces brutal error messages. Concepts and requires express the same constraints with readable syntax and far better diagnostics. Reach for SFINAE only to support pre-C++20 compilers, or for detection patterns concepts don't cover.

SFINAE Core Concept

When the compiler tries to substitute template arguments and fails, that's not an error - the template is simply removed from the overload set.

// Works for types with .size() method
template<typename T>
auto getSize(const T& container)
-> decltype(container.size())
{
return container.size();
}

// Works for array types
template<typename T, size_t N>
size_t getSize(const T (&array)[N]) {
return N;
}

std::vector<int> vec{1, 2, 3};
int arr[5];

auto s1 = getSize(vec); // Calls first (vector has .size())
auto s2 = getSize(arr); // Calls second (array version)

If vec.size() didn't exist, the first template would SFINAE away, not cause an error.

How SFINAE Works

// This will SFINAE away for types without ::value_type
template<typename T>
typename T::value_type get(const T& container) {
return container[0];
}

// Fallback for types without ::value_type
template<typename T>
T get(const T& value) {
return value;
}

std::vector<int> vec{42};
int x = 42;

auto a = get(vec); // Uses first (vector has value_type)
auto b = get(x); // Uses second (int doesn't, SFINAE)

Process:

  1. Compiler tries first template with int
  2. Substitution fails: int::value_type doesn't exist
  3. SFINAE: Remove first template, no error
  4. Try second template: Success!

Expression SFINAE

Check if expressions compile without actually compiling them.

// Check if T supports operator[]
template<typename T>
auto access(T& container, size_t index)
-> decltype(container[index]) // SFINAE on this
{
return container[index];
}

// Fallback
template<typename T>
auto access(T& value, size_t)
-> decltype(value)
{
return value;
}

std::vector<int> vec{1, 2, 3};
int x = 42;

access(vec, 1); // Returns vec[1]
access(x, 0); // Returns x (no operator[])

Trailing Return Type SFINAE

Perfect for SFINAE - checked during substitution.

// Only enabled if T has .begin() and .end()
template<typename T>
auto print(const T& container)
-> decltype(container.begin(), container.end(), void())
{
for (const auto& item : container) {
std::cout << item << " ";
}
}

// Fallback
void print(...) { // Variadic catch-all
std::cout << "Not a container";
}

std::vector<int> vec{1, 2, 3};
int x = 42;

print(vec); // "1 2 3"
print(x); // "Not a container"

Trick: (expr1, expr2, void()) checks multiple expressions and returns void.

Basic enable_if

std::enable_if is the standard tool for SFINAE-based template control.

#include <type_traits>

// Only enable for integral types
template<typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
increment(T value) {
return value + 1;
}

int x = increment(5); // int is integral
// double y = increment(3.14); // Removed from overload set

How it works:

  • std::enable_if<condition, T>::type exists only if condition is true
  • If false, substitution fails → SFINAE
  • Template removed, not an error

Modern enable_if (C++14)

// C++14: Shorter with _t alias
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T>
increment(T value) {
return value + 1;
}

// Even shorter with trailing return
template<typename T>
auto increment(T value)
-> std::enable_if_t<std::is_integral_v<T>, T>
{
return value + 1;
}

C++14 helpers:

  • enable_if_t<...> = shortcut for ::type
  • is_integral_v<T> = shortcut for ::value

Return Type enable_if

template<typename T>
std::enable_if_t<std::is_floating_point_v<T>, T>
sqrt(T value) {
return std::sqrt(value);
}

auto x = sqrt(3.14); // double
// auto y = sqrt(5); // int not floating point

Template Parameter enable_if

Cleaner - keeps function signature readable.

// Default template parameter approach
template<typename T,
std::enable_if_t<std::is_integral_v<T>, int> = 0>
T twice(T value) {
return value * 2;
}

// Or as extra parameter
template<typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
T triple(T value) {
return value * 3;
}

Multiple Overloads

Different implementations for different type categories.

// For integral types
template<typename T>
std::enable_if_t<std::is_integral_v<T>, void>
process(T value) {
std::cout << "Processing integer: " << value << "\n";
}

// For floating point types
template<typename T>
std::enable_if_t<std::is_floating_point_v<T>, void>
process(T value) {
std::cout << "Processing float: " << value << "\n";
}

process(42); // "Processing integer: 42"
process(3.14); // "Processing float: 3.14"

Class Templates

template<typename T, typename Enable = void>
class Container;

// Specialization for integral types
template<typename T>
class Container<T, std::enable_if_t<std::is_integral_v<T>>> {
public:
void info() { std::cout << "Integer container\n"; }
};

// Specialization for floating point
template<typename T>
class Container<T, std::enable_if_t<std::is_floating_point_v<T>>> {
public:
void info() { std::cout << "Float container\n"; }
};

Container<int> c1;
c1.info(); // "Integer container"

Container<double> c2;
c2.info(); // "Float container"

Combining Conditions

// Multiple requirements
template<typename T>
std::enable_if_t<std::is_arithmetic_v<T> && !std::is_same_v<T, bool>, T>
process(T value) {
return value * 2;
}

// std::conjunction (C++17)
template<typename T>
std::enable_if_t<std::conjunction_v
std::is_arithmetic<T>,
std::negation<std::is_same<T, bool>>
>, T>
process2(T value) {
return value * 2;
}

Detection Idiom

Check if types have specific members.

// Primary template: assume false
template<typename, typename = void>
struct has_size : std::false_type {};

// Specialization: true if T::size() exists
template<typename T>
struct has_size<T, std::void_t<decltype(std::declval<T>().size())>>
: std::true_type {};

// Helper variable template
template<typename T>
constexpr bool has_size_v = has_size<T>::value;

// Usage
has_size_v<std::vector<int>> // true
has_size_v<int> // false

How it works:

  • std::void_t turns any type into void
  • If T::size() doesn't exist, substitution fails → SFINAE
  • Falls back to primary template (false_type)

std::void_t Explained

// std::void_t always produces void
template<typename...>
using void_t = void;

// Useful for SFINAE
template<typename T, typename = void>
struct has_foo : std::false_type {};

template<typename T>
struct has_foo<T, std::void_t<decltype(&T::foo)>>
: std::true_type {};

// If T::foo exists, void_t<...> produces void → matches specialization
// Otherwise, SFINAE removes specialization, uses primary

Common Type Traits

std::is_integral<T> // int, long, char, bool
std::is_floating_point<T> // float, double
std::is_arithmetic<T> // integral or floating
std::is_pointer<T> // pointer types
std::is_reference<T> // reference types
std::is_const<T> // const qualified
std::is_class<T> // class or struct
std::is_enum<T> // enum types

std::is_same<T, U> // T and U are same type
std::is_base_of<Base, Derived> // Inheritance check
std::is_convertible<From, To> // Can convert?

Concepts (C++20) - Modern Alternative

Concepts are cleaner and more readable than SFINAE/enable_if.

// Old way with enable_if
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T>
twice(T value) {
return value * 2;
}

// New way with concepts
template<std::integral T>
T twice(T value) {
return value * 2;
}

// Or with requires clause
template<typename T>
requires std::integral<T>
T twice(T value) {
return value * 2;
}

Much more readable! See the Concepts section for details.

Summary

SFINAE fundamentals:

  • Substitution Failure Is Not An Error
  • Failed substitution removes template, doesn't error
  • Enables conditional template compilation
  • Foundation for template metaprogramming

enable_if usage:

  • enable_if<condition, T>::type exists only if condition true
  • Use enable_if_t (C++14) for brevity
  • Common positions: return type, template parameter, function parameter
  • Combine with type traits for powerful conditions

Common patterns:

  • Multiple overloads for different type categories
  • Detection idiom with void_t for member checking
  • Expression SFINAE with decltype
  • Trailing return types for SFINAE

Modern alternatives:

  • Concepts (C++20) - cleaner, better errors
  • if constexpr (C++17) - simpler branching
  • Prefer these over SFINAE when available