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.
SFINAE: When template substitution fails, remove template from consideration instead of erroring
enable_if: Enable/disable templates based on compile-time conditions
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:
- Compiler tries first template with
int - Substitution fails:
int::value_typedoesn't exist - SFINAE: Remove first template, no error
- 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>::typeexists 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::typeis_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_tturns any type intovoid- 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>::typeexists 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_tfor 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