Skip to content
C++
Idiom
since C++11
Advanced

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++11

A 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 β€” decltype and std::declval made expression-validity checks practical.
  • C++17 β€” std::void_t canonicalized the two-template pattern into a single, well-understood idiom.
  • Library Fundamentals TS v2 β€” std::experimental::is_detected built a reusable abstraction on top of void_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):

cpp
// 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:

cpp
// 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

cpp
#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

cpp
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:

cpp
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:

cpp
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:

cpp
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

cpp
// 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:

cpp
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.

cpp
// --- 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:

cpp
// 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:

cpp
#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:

cpp
// 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

SituationRecommended approach
C++11 or C++14 codebaseRaw SFINAE with decltype + expression SFINAE
C++17 codebasestd::void_t + nonesuch/detector pattern
C++17, many traits to writeis_detected / is_detected_v family (homebrew or TS v2)
C++20+ codebase, new codeNamed 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 fallbackdetected_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:

cpp
// 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); }
cpp
// 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.

cpp
// 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.

cpp
// 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:

cpp
// <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::experimental

Despite 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.

cpp
// 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:

cpp
// 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.

cpp
// 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 pattern
  • std::experimental::is_detected β€” Library Fundamentals TS v2 reusable abstraction
  • std::enable_if β€” C++11 building block for conditional overload sets
  • C++20 requires expressions β€” the modern, concept-native successor to expression-validity detection
  • C++20 concept subsumption β€” ordering rules that make multi-overload dispatch work without negated enable_if conditions