C++11: std::enable_if C++14: std::enable_if_t
The previous page ended on a distinction: static_assert is a wall, but sometimes you need a filter — a way to make a function template simply not exist for the wrong types, so overload resolution flows around it to a better candidate. For a decade, std::enable_if was how C++ spelled that. Concepts have since taken the job (and if constexpr, next page, took a related one), but enable_if is still everywhere: in every major library's internals, in every pre-C++20 codebase, and in every error message you'll ever debug through them.
The rule it rides on: SFINAE
When the compiler considers a function template for a call, it substitutes the deduced template arguments into the declaration. If that substitution produces invalid code — in the immediate context of the declaration — the language rule is Substitution Failure Is Not An Error: the template is silently dropped from the candidate set instead of failing the build.
template<typename T>
typename T::value_type first_of(const T& c) { return *c.begin(); }
int first_of(int x) { return x; }
first_of(std::vector<int>{1, 2}); // template: T::value_type is int - viable
first_of(7); // template: int::value_type is invalid ->
// dropped without error; plain overload wins
SFINAE happens whether or not you invited it. enable_if is just a device for triggering it on purpose, from a condition you choose.
What enable_if actually is
The entire facility is four lines — worth reading once, because it demystifies everything built on top:
template<bool B, typename T = void>
struct enable_if {}; // false: no ::type member
template<typename T>
struct enable_if<true, T> { using type = T; }; // true: ::type is T
enable_if<true, int>::type is int. enable_if<false, int>::type does not exist — and naming a member that doesn't exist is a substitution failure. Put enable_if_t<condition, ...> anywhere in a template's declaration, and the whole template evaporates from overload resolution exactly when the condition is false. (C++14 added the enable_if_t alias so code stops chanting typename ... ::type.)
Where to put the condition
Three placements appear in real code. They behave identically when there's one overload — the differences surface the moment there are two:
| Idiom | Shape | Weakness |
|---|---|---|
| Return type | std::enable_if_t<cond, T> f(...) |
buries the signature; unusable for constructors |
| Defaulted type parameter | template<typename T, typename = std::enable_if_t<cond>> |
mutually exclusive overloads collide (below) |
| Non-type parameter | template<typename T, std::enable_if_t<cond, int> = 0> |
none worth mentioning — the recommended form |
The recommended form in action:
#include <print>
#include <type_traits>
template<typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
void describe(T) { std::println("integral"); }
template<typename T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
void describe(T) { std::println("floating point"); }
int main() {
describe(42); // integral
describe(2.5); // floating point
// describe("hi"); // error: no matching function - both filtered out
}
When the condition is false, enable_if_t has no type to produce, the anonymous non-type parameter can't be declared, and that overload is gone. Each call sees exactly one survivor. Note the last line: filtering every candidate out is a feature — describe genuinely does not exist for pointers, and the error says so at the call site.
The redefinition trap
Why not the tidier-looking defaulted type parameter? Because default template arguments are not part of a template's signature. Write two overloads that differ only there, and you haven't written two overloads — you've defined the same template twice:
template<typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
void describe(T);
template<typename T, typename = std::enable_if_t<std::is_floating_point_v<T>>>
void describe(T); // error: redefinition of 'template<class T, class> void describe(T)'
The non-type idiom dodges this because the conditions live in the parameter's type, and enable_if_t<is_integral_v<T>, int> and enable_if_t<is_floating_point_v<T>, int> are different expressions — distinct signatures, legal overloads. This single trap is the reason the int = 0 spelling became the community standard.
Constraining a class template
Classes don't have overload resolution, but they have partial specialization — and SFINAE applies when the compiler tries to match a partial specialization's arguments. A defaulted extra parameter gives the condition somewhere to live:
#include <print>
#include <type_traits>
// Primary template: chosen whenever no specialization matches.
template<typename T, typename Enable = void>
struct Storage {
static constexpr const char* strategy = "inline buffer";
};
template<typename T>
struct Storage<T, std::enable_if_t<(sizeof(T) > 16)>> {
static constexpr const char* strategy = "heap allocation";
};
struct Small { int x; };
struct Big { double a, b, c; };
int main() {
std::println("Small: {}", Storage<Small>::strategy);
std::println("Big: {}", Storage<Big>::strategy);
}
Storage<Big> deduces the specialization's second argument as enable_if_t<true> — that's void, matching the default, so the specialization wins. For Small the substitution fails, the specialization is discarded, and the primary template serves. Selecting an entire class layout by a compile-time property of T is enable_if doing something if constexpr cannot: it changes which type exists, not which branch runs.
Constructors — the place with no return type
Constructors can't use the return-type idiom and often can't add function parameters, which makes the non-type template parameter form the only clean option — this is where you'll meet it most in library code:
class Seconds {
public:
template<typename N, std::enable_if_t<std::is_arithmetic_v<N>, int> = 0>
explicit Seconds(N count) : count_(static_cast<double>(count)) {}
private:
double count_;
};
Without the constraint, Seconds s{some_string}; would fail inside the constructor with a conversion error; with it, the constructor never matches and the error lands at the call site, pointing at the actual mistake.
What replaced it
C++20 Concepts do the same filtering with a fraction of the syntax, and the compiler understands what you meant — constraint failures produce "constraint not satisfied" notes naming the requirement, not substitution archaeology:
#include <concepts>
void describe(std::integral auto) { std::println("integral"); }
void describe(std::floating_point auto) { std::println("floating point"); }
Where the goal was never to steer overload resolution but to pick a branch inside one function, C++17's if constexpr — the next page — replaced whole enable_if overload sets with an ordinary-looking if. New code should reach for those. Read enable_if fluently anyway: the standard library's own headers are full of it, and "why doesn't this overload get chosen" bugs in existing code will be yours to solve.
Guidelines
- In new C++20 code, write constraints as concepts (
std::integral auto,requires-clauses); reserveenable_iffor codebases and public APIs stuck on C++17 or below. - When you do write it, use the non-type parameter idiom —
std::enable_if_t<cond, int> = 0— everywhere, including constructors. It's the one placement without a trap. - Never write two overloads differing only in a defaulted type parameter; that's a redefinition, not an overload set.
- Use
enable_if(or concepts) when the wrong types should make a function not exist; usestatic_assertwhen they should produce your error message; the difference is whether another overload deserves a chance. - Partial specialization plus
enable_ifremains the tool for choosing between class implementations by type properties —if constexpronly selects statements, not members or layouts.