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.
The Full is_detected Family (Library Fundamentals TS v2)
The existing page shows the core detector / nonesuch implementation but the TS v2 proposal provides several additional aliases that make call-sites even cleaner. None of these are in the IS, but all are portable to any C++17 toolchain via the implementation above.
// --- additional aliases on top of detector / nonesuch ---
// true/false: is Op<Args...> a valid type?
template<template<class...> class Op, class... Args>
constexpr bool is_detected_v = is_detected<Op, Args...>::value;
// Op<Args...> if valid, Default otherwise
template<class Default, template<class...> class Op, class... Args>
using detected_or_t = typename detector<Default, void, Op, Args...>::type;
// true iff Op<Args...> is exactly Expected
template<class Expected, template<class...> class Op, class... Args>
using is_detected_exact = std::is_same<Expected, detected_t<Op, Args...>>;
template<class Expected, template<class...> class Op, class... Args>
constexpr bool is_detected_exact_v =
is_detected_exact<Expected, Op, Args...>::value;
// true iff Op<Args...> is convertible to To
template<class To, template<class...> class Op, class... Args>
using is_detected_convertible =
std::is_convertible<detected_t<Op, Args...>, To>;
template<class To, template<class...> class Op, class... Args>
constexpr bool is_detected_convertible_v =
is_detected_convertible<To, Op, Args...>::value;Practical use of the extra aliases:
// Expression alias β one line per trait
template<class T>
using copy_assign_t = decltype(std::declval<T&>() = std::declval<const T&>());
struct Copyable {};
struct NoCopy { void operator=(const NoCopy&) = delete; };
static_assert( is_detected_v<copy_assign_t, Copyable>);
static_assert(!is_detected_v<copy_assign_t, NoCopy>);
// Exact return-type check
static_assert(is_detected_exact_v<Copyable&, copy_assign_t, Copyable>);
// Default fallback type: use T::difference_type if present, ptrdiff_t otherwise
template<class T>
using diff_t = typename T::difference_type;
template<class Ptr>
using difference_type = detected_or_t<std::ptrdiff_t, diff_t, Ptr>;
struct HasDiff { using difference_type = int; };
struct NoDiff {};
static_assert(std::is_same_v<difference_type<HasDiff>, int>);
static_assert(std::is_same_v<difference_type<NoDiff>, std::ptrdiff_t>);detected_or_t is especially valuable for SFINAE-friendly default-type selection in allocator traits, iterator traits, and similar customization-point infrastructure β without writing a single if constexpr or separate overload set.
C++20 requires Expressions as the Modern Replacement
C++20 requires expressions provide the same "is this expression valid?" capability directly in the language with significantly better ergonomics. The transition for the has_serialize running example across three standards illustrates the progression:
#include <ostream>
#include <type_traits>
// ββ C++11/14: raw SFINAE ββββββββββββββββββββββββββββββββββββββββββββββββββ
namespace cpp11 {
template<class T, class = void>
struct has_serialize : std::false_type {};
template<class T>
struct has_serialize<T,
decltype(std::declval<T>().serialize(std::declval<std::ostream&>()), void())
> : std::true_type {};
}
// ββ C++17: void_t + detection idiom ββββββββββββββββββββββββββββββββββββββ
namespace cpp17 {
template<class T, class = void>
struct has_serialize : std::false_type {};
template<class T>
struct has_serialize<T,
std::void_t<decltype(std::declval<T>().serialize(std::declval<std::ostream&>()))>
> : std::true_type {};
template<class T>
inline constexpr bool has_serialize_v = has_serialize<T>::value;
}
// ββ C++20: requires expression (ad-hoc) ββββββββββββββββββββββββββββββββββ
namespace cpp20_adhoc {
template<class T>
constexpr bool has_serialize =
requires(T t, std::ostream& os) { t.serialize(os); };
}
// ββ C++20: named Concept (preferred) βββββββββββββββββββββββββββββββββββββ
namespace cpp20 {
template<class T>
concept Serializable = requires(const T& t, std::ostream& os) {
{ t.serialize(os) } -> std::same_as<void>; // constrains return type too
};
}Using the concept in overload resolution via subsumption:
// C++20 β two overloads, no enable_if noise, subsumption handles ordering
template<cpp20::Serializable T>
void emit(const T& value, std::ostream& os) {
value.serialize(os); // called only when T satisfies the concept
}
template<class T>
void emit(const T& value, std::ostream& os) {
os << "(not serializable)\n"; // unconstrained fallback
}Concepts subsume each other: a more-constrained overload always wins over a less-constrained one without the programmer artificially ordering candidates via enable_if on both branches. SFINAE requires both overloads to explicitly negate each other's condition; concepts do not.
Error message comparison β why it matters:
With SFINAE, a constraint mismatch deep in a call chain produces multi-page substitution backtraces that point at internal trait machinery. With a named concept, the compiler says exactly which concept was not satisfied and which type violated it. This alone is reason enough to prefer concepts in C++20 codebases.
When to Use Each Approach
| Situation | Recommended approach |
|---|---|
| C++11 or C++14 codebase | Raw SFINAE with decltype + expression SFINAE |
| C++17 codebase | std::void_t + nonesuch/detector pattern |
| C++17, many traits to write | is_detected / is_detected_v family (homebrew or TS v2) |
| C++20+ codebase, new code | Named concept + requires expression |
| Portable library (C++14-compatible headers) | void_t + is_detected polyfill in a detail:: namespace |
| Checking exact return type (any standard) | is_detected_exact / { expr } -> std::same_as<R> in C++20 |
| Providing a default type fallback | detected_or_t<Default, Op, T> (C++17) |
The detection idiom and C++20 concepts are not mutually exclusive in a codebase: internal implementation headers that must support older compilers can use void_t, while public APIs targeting C++20 can expose named concepts built on the same semantic intent.
Migrating Detection Traits to C++20 Concepts
The detection idiom and C++20 concepts express the same semantic intent, so migration is mechanical. Each is_detected_v<has_serialize_t, T> usage maps directly to a concept check, and the concept typically reveals opportunities to add return-type constraints that were cumbersome with SFINAE.
The conversion pattern for the has_serialize running example:
// C++17 β detection idiom with is_detected_v
template<class T>
using has_serialize_t =
decltype(std::declval<T>().serialize(std::declval<std::ostream&>()));
template<class T>
inline constexpr bool has_serialize_v = is_detected_v<has_serialize_t, T>;
// Callsite: guard a function with the trait
template<class T, std::enable_if_t<has_serialize_v<T>, int> = 0>
void write(const T& v, std::ostream& os) { v.serialize(os); }// C++20 β named concept replaces the trait + enable_if guard
template<class T>
concept Serializable = requires(const T& v, std::ostream& os) {
{ v.serialize(os) } -> std::same_as<void>;
};
template<Serializable T>
void write(const T& v, std::ostream& os) { v.serialize(os); }When migrating, use is_detected_v as a temporary bridge: place a static_assert(is_detected_v<has_serialize_t, T> == Serializable<T>) in your test suite to confirm the concept and trait agree on all types in your test matrix before deleting the trait. This catches subtle differences introduced by the tighter return-type constraint in the concept form.
Detecting Non-Member Functions vs Member Functions
Real-world types expose operations through free functions as often as through members. A serialization protocol might require either t.serialize(os) or serialize(t, os) β and these require separate detection expressions, since SFINAE sees them as entirely different constructs.
// C++17
#include <type_traits>
#include <ostream>
// --- member function detection ---
template<class T>
using member_serialize_t =
decltype(std::declval<const T&>().serialize(std::declval<std::ostream&>()));
// --- non-member (ADL) function detection ---
// Must be placed in a namespace that does NOT itself contain serialize(),
// so that argument-dependent lookup only finds candidates from T's namespace.
namespace detail {
// Poison-pill overload: ensures the expression is ill-formed
// unless ADL finds a real overload in T's associated namespace.
void serialize(); // not defined; blocks unqualified fallback
template<class T>
using free_serialize_t =
decltype(serialize(std::declval<const T&>(),
std::declval<std::ostream&>()));
}
template<class T>
inline constexpr bool has_member_serialize_v =
is_detected_v<member_serialize_t, T>;
template<class T>
inline constexpr bool has_free_serialize_v =
is_detected_v<detail::free_serialize_t, T>;
// Composite: satisfies the protocol via either mechanism
template<class T>
inline constexpr bool is_serializable_v =
has_member_serialize_v<T> || has_free_serialize_v<T>;The poison-pill void serialize(); in the detail namespace is a well-known technique: it makes the unqualified call serialize(t, os) ill-formed when T's associated namespaces contribute no candidate, preventing a spurious true detection. Without it, a serialize visible from any enclosing namespace could satisfy the expression.
// C++20 β the same composite check as a concept
template<class T>
concept Serializable =
requires(const T& v, std::ostream& os) { v.serialize(os); } ||
requires(const T& v, std::ostream& os) { serialize(v, os); };Concepts handle the ADL case correctly without poison pills because requires expressions evaluate the call in a context where only ADL and explicit using declarations apply β the same semantics customization-point objects (CPOs) rely on.
std::experimental::is_detected β The Proposed Standard API
Library Fundamentals TS v2 codified the nonesuch/detector pattern under the std::experimental namespace, making it the closest thing to a standard spelling before C++20 concepts. The specification lives in the <experimental/type_traits> header, which GCC's libstdc++ has shipped since GCC 6 and libc++ since LLVM 4.
The header exposes these names under namespace std::experimental:
// <experimental/type_traits> β Library Fundamentals TS v2
// (illustrative; actual definition in vendor headers)
namespace std::experimental {
// Sentinel type β deliberately non-constructible and non-destructible
struct nonesuch { /* ... */ };
// Primary detection alias
template<template<class...> class Op, class... Args>
using is_detected = /* implementation-defined */;
template<template<class...> class Op, class... Args>
constexpr bool is_detected_v = is_detected<Op, Args...>::value;
// Detected type β nonesuch when Op<Args...> is ill-formed
template<template<class...> class Op, class... Args>
using detected_t = /* Op<Args...> or nonesuch */;
// Detected type with fallback
template<class Default, template<class...> class Op, class... Args>
using detected_or_t = /* Op<Args...> if valid, Default otherwise */;
// Exact return-type check
template<class Expected, template<class...> class Op, class... Args>
using is_detected_exact = std::is_same<Expected, detected_t<Op, Args...>>;
// Convertibility check
template<class To, template<class...> class Op, class... Args>
using is_detected_convertible =
std::is_convertible<detected_t<Op, Args...>, To>;
} // namespace std::experimentalDespite strong momentum, the TS v2 paper (N4562) was not merged into C++20 β the committee redirected effort toward concepts, which make is_detected largely redundant in new code. The TS names are therefore not guaranteed to remain in <experimental/type_traits> beyond any given vendor's implementation; treat them as a useful polyfill rather than a long-lived API surface. In production code, prefer either the homebrew detector/nonesuch implementation (shown earlier in this page) or a vendored copy from a library such as Abseil's absl::type_traits_internal to avoid dependency on experimental headers.
Compile-Time Feature Detection in Library Code
Large portable libraries use the detection idiom as a compatibility shim β detecting which operations a user-supplied type provides and dispatching to the richest available implementation. This is the technique behind range-v3's optional size() fast-path, Abseil's absl::Hash support, and the std::hash specialization machinery in libc++.
A typical pattern: offer a fast path when T::size() is O(1), fall back to std::distance otherwise.
// C++17 β range-v3-style O(1) size detection
template<class R>
using member_size_t = decltype(std::declval<const R&>().size());
template<class R>
using free_size_t = decltype(size(std::declval<const R&>()));
template<class R>
inline constexpr bool has_constant_size_v =
is_detected_v<member_size_t, R> || is_detected_v<free_size_t, R>;
template<class R, std::enable_if_t< has_constant_size_v<R>, int> = 0>
auto range_size(const R& r) { return r.size(); } // O(1)
template<class R, std::enable_if_t<!has_constant_size_v<R>, int> = 0>
auto range_size(const R& r) { // O(n) fallback
return std::distance(std::begin(r), std::end(r));
}Abseil's absl::Hash uses a similar technique to detect whether a user type provides AbslHashValue as a hidden friend or free function, enabling opt-in hashing without inheriting from a base class:
// C++17 β simplified version of Abseil's hash-extension detection
namespace hash_internal {
void AbslHashValue(); // poison-pill
template<class H, class T>
using AbslHashValue_t =
decltype(AbslHashValue(std::declval<H>(), std::declval<const T&>()));
}
template<class H, class T>
inline constexpr bool has_absl_hash_value_v =
is_detected_v<hash_internal::AbslHashValue_t, H, T>;The detection idiom also appears in backwards-compatibility layers: when a library adds a new optional protocol (such as a reserve() hint to pre-allocate output containers), it uses detection to call reserve() when present and skip it silently when absent, keeping the library compatible with containers predating the protocol.
// C++17 β optional reserve() protocol
template<class C>
using has_reserve_t =
decltype(std::declval<C&>().reserve(std::declval<std::size_t>()));
template<class C>
void try_reserve(C& c, std::size_t n) {
if constexpr (is_detected_v<has_reserve_t, C>) { // C++17
c.reserve(n);
}
}This pattern is so common in C++17 library internals that it is sometimes called the optional protocol idiom β a contract that a type may satisfy to unlock a performance path, without being forced to satisfy it for correctness.
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 - C++20 concept subsumption β ordering rules that make multi-overload dispatch work without negated
enable_ifconditions