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.
The Primary Template (Default Fallback): A base template defines the fallback behavior, usually inheriting from
std::false_type. It takes the target typeTand an unused second type parameter that defaults tovoid.template <typename T, typename = void> struct has_serialize : std::false_type {};The Partial Specialization (Detection Path): A partial specialization sets the second parameter to
std::void_t<...>. Insidestd::void_t, you place an expression that accesses the member you wish to detect. This specialization inherits fromstd::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:
- The compiler inspects
has_serialize<MyClass, void>. - It attempts to instantiate the partial specialization by evaluating
the expression
decltype(std::declval<T>().serialize()). - If the member exists: The
decltypeexpression yields a valid return type.std::void_t<ValidType>resolves tovoid. The specialization matcheshas_serialize<MyClass, void>. Because a partial specialization is more specialized than the primary template, the compiler selects it, yieldingstd::true_type. - 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.