Library code and older codebases are full of these techniques, and they explain how the standard library makes compile-time decisions.
Template metaprogramming uses templates to compute with types and constants at compile time. The standard toolkit lives in <type_traits>: predicates like std::is_integral_v<T>, std::is_trivially_copyable_v<T> and std::is_base_of_v<B, D>; transformations like std::remove_cvref_t<T>, std::decay_t<T> and std::conditional_t<cond, A, B>; and std::integral_constant / std::bool_constant as the base for writing your own traits.
SFINAE, "substitution failure is not an error", is the rule that makes the old techniques work. While the compiler substitutes template arguments into a candidate's signature, an invalid type or expression just removes that candidate from overload resolution instead of causing an error. Errors inside the function body are still hard errors.
std::enable_if_t<cond, int> = 0as a template parameter enables an overload only whencondholds.- The detection idiom uses partial specialisation with
std::void_t<decltype(expr)>to ask "does this expression compile?": if it does, the specialisation that inherits fromstd::true_typeis chosen. decltypeandstd::declval<T>()let you form expressions on types without creating objects.
C++17 if constexpr removed much of the need for overload tricks: the discarded branch isn't instantiated, so it may contain code that wouldn't compile for that T. Fold expressions replaced most recursive variadic templates.
C++20 concepts replace the rest. template <std::integral T> says what enable_if used to say, with better errors and real subsumption rules, and a requires expression is the detection idiom built into the language. Write new code with concepts and if constexpr; learn SFINAE to read and maintain existing libraries, and for the rare cases where you still need a trait class.
Example
#include <iostream>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
// SFINAE: each overload exists only if its enable_if condition holds
template <typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
std::string kind(T) { return "integral"; }
template <typename T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
std::string kind(T) { return "floating"; }
// Detection idiom: does `t.size()` compile for T?
template <typename T, typename = void>
struct has_size : std::false_type {};
template <typename T>
struct has_size<T, std::void_t<decltype(std::declval<const T&>().size())>> : std::true_type {};
template <typename T>
auto length(const T& x) {
if constexpr (has_size<T>::value) return x.size(); // other branch not instantiated
else return sizeof(T);
}
int main() {
std::cout << kind(1) << ' ' << kind(1.0) << '\n'; // integral floating
std::cout << length(std::vector<int>(5)) << ' ' << length(42) << '\n'; // 5 4
static_assert(has_size<std::string>::value && !has_size<int>::value);
static_assert(requires(std::string s) { s.size(); }); // C++20 way to ask the same
}Remember
- SFINAE: invalid substitution in a signature drops the candidate silently
<type_traits>+decltype/declvalare the metaprogramming toolkit- Prefer concepts and
if constexprin new code
Watch out for
- Expecting SFINAE to apply to errors inside a function body — only the immediate signature context is SFINAE-friendly.