This is a record, mostly for my own benefit, of different ways to write the detection idiom in C++. If you're looking for a tutorial-style guide to the detection idiom, I highly recommend Sy Brand's blog post about the detection idiom as a stand-in for concepts.
The detection idiom is what allows compile-time type introspection in C++. Using the detection idiom, we can check if a type has methods, type aliases, or members that fit a particular interface. This allows library writers to create metaprogramming facilities that are much more expressive. The user can plug any type she likes into a metafunction as long as it fits the required interface.
C++ 20 will have a first-class facility for compile-type type introspection called concepts. Concepts are more concise and explicit than current iterations of the detection idiom and will result in better compiler errors. For now, though, we emulate concepts using the detection idiom. All current iterations of the detection idiom depend on SFINAE, which is the property that certain errors that occur during overload resolution are not fatal errors, but just result in substitution failure, with the compiler going on to try the next candidate. We can use this property to check whether a type has a particular interface, returning true if it does and false if it does not.
C++ Fundamentals TS v2 provides a super easy way to use the detection
idiom in C++ through is_detected
.
All the user needs to do is write a template struct that checks for
the attributes the user wants, then hand that off to is_detected
.
Here's an example, where we want to ensure that a type has a method get()
template <typename T>
using get_t = decltype(std::declval<T>().get());
template <typename >
using supports_get = std::experimental::is_detected<get_t, T>;
main() {
std::cout << supports_get<foo>::value << std::endl;
...
}
You can use your new supports_get
template metafunction to create
partial specializations, SFINAE out, etc. You can use conjunction
to check for multiple traits and is_same
to check for the return
value. Both of these are in type_traits
.
The downside of this is that either your compiler needs to include
<experimental/type_traits>
, or you need to include the
the implementation of is_detected
from cppreference.
In an older style of the detection idiom, you create a single class
and use expression SFINAE on the return type of methods in that class
to return true if the method is detected and false if the method
does not exist. In the example below, we're calling test_get
with an int
parameter. This causes the first test_get
method, which takes an int
as an argument, to be preferred
over the second test_get
method, which takes anything (...
)
as an argument. The first method will only be enabled, however, if
the expression inside the decltype
can resolve without errors.
The return type for both functions is bool, since decltype
will return the type of the last expression (technically, this is
due to the comma operator).
template <typename T>
struct has_get {
template <typename U>
static constexpr
decltype(std::declval<U>().get(), bool())
test_get(int) {
return true;
}
template <typename U>
static constexpr bool test_get(...) {
return false;
}
static constexpr bool value = test_get<T>(int());
};
If we restrict ourselves to pre-C++11, we lose decltype
,
which is the main driver of the detection idiom patterns above.
We can, however, fairly easily emulate this by abusing sizeof
.
template <typename T>
T declval();
template <typename T>
struct has_get {
typedef char yes[1];
typedef char no[2];
template <typename U>
static yes& test_get(int (*)[sizeof(declval<U>().get(), 1)]);
template <typename U>
static no& test_get(...);
static const bool value = sizeof(test_get<T>(NULL)) == sizeof(yes);
};
Note that here we're using the size of the return value to check how
the overloaded test_get
function is resolved. Also,
we have to define our own declval
, since that does not
exist pre-C++11. In the first test_get
, we're passing a
pointer to a fixed size array int (*) [x]
, where x
,
the size of the array, is determined by our sizeof
expression.
Similar to the decltype
expressions above, this will SFINAE
out if our type does not have the method get()
, and will return
1
otherwise.