C++ Detection Idiom with std::void_t

The detection idiom in C++ leverages std::void_t alongside Substitution Failure Is Not An Error (SFINAE) to determine at compile time whether a class exposes a specific type, member variable, or member function. By attempting to form a type expression within a template specialization, the compiler can inspect a class's interface and selectively enable different code paths based on whether that member exists.

What is std::void_t?

Introduced in C++17 (and implementable in C++11 with a simple alias), std::void_t is a template alias that maps any sequence of types to void:

template <typename...>
using void_t = void;

While seemingly trivial, std::void_t is powerful because the compiler must validate every template argument passed into it. If any type expression inside std::void_t<...> is invalid or ill-formed, the substitution fails. Thanks to SFINAE, this failure does not halt compilation with an error; instead, the compiler quietly discards that particular specialization and searches for an alternative match.

The Mechanics of the Detection Idiom

The detection idiom pairs a primary template with a partial specialization using std::void_t.

  1. The Primary Template (Default Fallback): A base template defines the fallback behavior, usually inheriting from std::false_type. It takes the target type T and an unused second type parameter that defaults to void.

    template <typename T, typename = void>
    struct has_serialize : std::false_type {};
  2. The Partial Specialization (Detection Path): A partial specialization sets the second parameter to std::void_t<...>. Inside std::void_t, you place an expression that accesses the member you wish to detect. This specialization inherits from std::true_type.

    template <typename T>
    struct has_serialize<T, std::void_t<decltype(std::declval<T>().serialize())>>
        : std::true_type {};

How the Compiler Resolves the Template

When you query has_serialize<MyClass>::value:

  1. The compiler inspects has_serialize<MyClass, void>.
  2. It attempts to instantiate the partial specialization by evaluating the expression decltype(std::declval<T>().serialize()).
  3. If the member exists: The decltype expression yields a valid return type. std::void_t<ValidType> resolves to void. The specialization matches has_serialize<MyClass, void>. Because a partial specialization is more specialized than the primary template, the compiler selects it, yielding std::true_type.
  4. If the member does not exist: Evaluating the expression produces an invalid expression. SFINAE triggers, discarding the specialization without generating an error. The compiler falls back to the primary template, which inherits from std::false_type.

Practical Example

The following complete example demonstrates how to check for a nested type, a member variable, and a member function:

#include <iostream>
#include <type_traits>
#include <utility>

// 1. Detect a member function: t.draw()
template <typename T, typename = void>
struct has_draw : std::false_type {};

template <typename T>
struct has_draw<T, std::void_t<decltype(std::declval<T>().draw())>> 
    : std::true_type {};

// 2. Detect a member variable: t.id
template <typename T, typename = void>
struct has_id : std::false_type {};

template <typename T>
struct has_id<T, std::void_t<decltype(std::declval<T>().id)>> 
    : std::true_type {};

// Helper variable templates (C++14/17 style)
template <typename T>
inline constexpr bool has_draw_v = has_draw<T>::value;

template <typename T>
inline constexpr bool has_id_v = has_id<T>::value;

struct Circle {
    int id;
    void draw() {}
};

struct Dummy {};

int main() {
    std::cout << std::boolalpha;
    std::cout << "Circle has draw: " << has_draw_v<Circle> << '\n'; // true
    std::cout << "Circle has id:   " << has_id_v<Circle> << '\n';   // true
    std::cout << "Dummy has draw:  " << has_draw_v<Dummy> << '\n';  // false
    std::cout << "Dummy has id:    " << has_id_v<Dummy> << '\n';   // false
}

By combining declval, decltype, and std::void_t, the detection idiom provides a robust, non-intrusive mechanism to inspect class interfaces at compile time, laying the groundwork for trait-based dispatch and conditional compilation via if constexpr.