Detection Idiom
Compile-time trait detection via SFINAE and void_t to test whether a type supports a given expression, member, or nested type.
Detection Idiomsince C++11A template metaprogramming technique that uses SFINAE and partial specialization to determine at compile time whether a type satisfies a given constraint β a member function, a nested type, a valid expression β without producing a hard error.
Overview
The detection idiom answers the question: does type T support operation X? β at compile time, without committing to a call that might fail. It exploits SFINAE (Substitution Failure Is Not An Error): when deducing template arguments produces an ill-formed expression, that specialization is silently dropped rather than triggering a diagnostic. By pairing a primary template that derives from std::false_type with a partial specialization that derives from std::true_type and will only be selected when the expression is valid, you get a boolean trait.
The idiom matured across standards:
- C++11 β
decltypeandstd::declvalmade expression-validity checks practical. - C++17 β
std::void_tcanonicalized the two-template pattern into a single, well-understood idiom. - Library Fundamentals TS v2 β
std::experimental::is_detectedbuilt a reusable abstraction on top ofvoid_t. - C++20 β Concepts express the same constraints directly with far better error messages and should be preferred in new code.
The idiom remains relevant in C++11/14/17 codebases, backward-compatible library headers, and polyfill infrastructure.
Syntax
The canonical form relies on std::void_t (C++17):
// Primary template β wins when substitution fails
template<class T, class = void>
struct has_serialize : std::false_type {};
// Partial specialization β wins when the expression is well-formed
template<class T>
struct has_serialize<T,
std::void_t<decltype(std::declval<T>().serialize(std::declval<std::ostream&>()))>
> : std::true_type {};std::void_t<Exprs...> (C++17) maps any well-formed sequence of types to void. If any Expr is ill-formed, the partial specialization is not a candidate and the primary template wins. If all are well-formed, the partial specialization wins by being more specialised.
On C++11 and C++14, a compiler-bug workaround is required due to CWG 1558 β a defect that permitted compilers to ignore unused alias template parameters, making the naive template<typename...> using void_t = void; silently unreliable:
// C++11/14 workaround (CWG 1558)
template<typename... Ts>
struct make_void { using type = void; };
template<typename... Ts>
using void_t = typename make_void<Ts...>::type;Examples
Detecting a member function
#include <type_traits>
#include <string>
template<class T, class = void>
struct has_to_string : std::false_type {};
template<class T>
struct has_to_string<T,
std::void_t<decltype(std::declval<const T&>().to_string())>
> : std::true_type {};
// C++17: inline variable template for ergonomics
template<class T>
inline constexpr bool has_to_string_v = has_to_string<T>::value;
struct Widget { std::string to_string() const { return "Widget"; } };
struct Gadget {};
static_assert( has_to_string_v<Widget>); // C++17
static_assert(!has_to_string_v<Gadget>);Detecting a nested type
template<class T, class = void>
struct has_value_type : std::false_type {};
template<class T>
struct has_value_type<T, std::void_t<typename T::value_type>>
: std::true_type {};
static_assert( has_value_type<std::vector<int>>::value);
static_assert(!has_value_type<double>::value);The is_detected abstraction
Repeating the two-template boilerplate for every check is noise. The is_detected family (from Library Fundamentals TS v2, not in the IS) abstracts the mechanism into a reusable combinator:
struct nonesuch {
~nonesuch() = delete;
nonesuch() = delete;
nonesuch(nonesuch const&) = delete;
void operator=(nonesuch const&) = delete;
};
template<class Default, class AlwaysVoid,
template<class...> class Op, class... Args>
struct detector {
using value_t = std::false_type;
using type = Default;
};
template<class Default, template<class...> class Op, class... Args>
struct detector<Default, std::void_t<Op<Args...>>, Op, Args...> { // C++17
using value_t = std::true_type;
using type = Op<Args...>;
};
template<template<class...> class Op, class... Args>
using is_detected = typename detector<nonesuch, void, Op, Args...>::value_t;
template<template<class...> class Op, class... Args>
using detected_t = typename detector<nonesuch, void, Op, Args...>::type;
template<class Default, template<class...> class Op, class... Args>
using detected_or_t = typename detector<Default, void, Op, Args...>::type;Each new trait then requires only an alias describing the expression β no boilerplate:
template<class T>
using serialize_expr = decltype(std::declval<T>().serialize(std::declval<std::ostream&>()));
template<class T>
using has_serialize = is_detected<serialize_expr, T>;
// Fallback type when not detected
template<class T>
using serialize_result_or_void = detected_or_t<void, serialize_expr, T>;Compile-time dispatch with if constexpr
Detection becomes most useful when paired with branching. C++17's if constexpr collapses two overloads into a single function body:
template<class T>
void log_value(const T& v) {
if constexpr (has_to_string_v<T>) { // C++17
std::cout << v.to_string() << '\n';
} else {
std::cout << "(no string representation)\n";
}
}Pre-C++17, the same branching required two std::enable_if-constrained overloads.
C++20: concepts supersede the idiom
// C++20
template<class T>
concept ToStringable = requires(const T& t) {
{ t.to_string() } -> std::convertible_to<std::string>;
};
template<ToStringable T>
void log_value(const T& v) { std::cout << v.to_string() << '\n'; }
template<class T>
void log_value(const T&) { std::cout << "(no string representation)\n"; }Concepts produce actionable error messages, handle overload resolution cleanly, and do not require wrapping every check in a separate trait struct. Prefer them in C++20 and later; use the detection idiom only when cross-version compatibility matters.
Best Practices
Constrain the expression, not just the name. A member function with the right name but wrong return type satisfies a naive trait. When the return type matters, fold it into the detection expression:
template<class T>
using to_string_expr = std::enable_if_t<
std::is_convertible_v<decltype(std::declval<const T&>().to_string()), std::string>
>;Choose declval<T&>() vs declval<T>() deliberately. declval<T>() produces an rvalue; declval<T&>() produces an lvalue. A trait checking operator<< on a mutable stream must use lvalue references for both the stream and the object being tested.
Centralise detection machinery. Once nonesuch and detector are in a utilities header, every new trait is two lines. Avoid repeating the two-template pattern across dozens of traits.
Expose _v variable templates. Trait structs with ::value are verbose at call sites. Provide inline constexpr bool has_X_v = has_X<T>::value; (C++17) for every trait you ship.
Common Pitfalls
Hard errors inside function bodies are not SFINAE. SFINAE applies only in the immediate context of template argument deduction β chiefly in function signatures and class template specializations. A substitution failure inside a function body is a hard error. std::void_t only protects expressions that appear directly in the specialization's template parameter list.
Forgetting std::declval. decltype(T().member()) fails for non-default-constructible types. Always write decltype(std::declval<T>().member()).
Shadowing with const correctness. std::declval<T>() produces T&&. If you want to test a const-qualified member, use std::declval<const T&>() β otherwise you may detect a mutable overload and miss that the const one is absent.
CWG 1558 on older toolchains. GCC before 5 and Clang before 3.6 may silently accept a trivial void_t alias and then ignore it during specialization matching. Use the make_void indirection if supporting pre-C++17 compilers.
See Also
std::void_tβ C++17 standard utility that canonicalized this patternstd::experimental::is_detectedβ Library Fundamentals TS v2 reusable abstractionstd::enable_ifβ C++11 building block for conditional overload sets- C++20
requiresexpressions β the modern, concept-native successor to expression-validity detection