Detection Idiom - A Stopgap for Concepts
Concepts is a proposed C++ feature which allows succinct, expressive, and powerful constraining of templates. They have been threatening to get in to C++ for a long time now, with the first proposal being rejected for C++11. They were finally merged in to C++20 a few months ago, which means we need to hold on for another few years before they’re in the official standard rather than a Technical Specification. In the mean time, there have been various attempts to implement parts of concepts as a library so that we can have access to some of the power of Concepts without waiting for the new standard. The detection idiom – designed by Walter Brown and part of the Library Fundamentals 2 Technical Specification – is one such solution. This post will outline the utility of it, and show the techniques which underlie its implementation.
A problem
We are developing a generic library. At some point on our library we need to calculate the foo
factor for whatever types the user passes in.
Some types will have specialized implementations of this function, but for types which don’t we’ll need some generic calculation.
Using concepts we could write calculate_foo_factor
like so:
This is quite succinct and clear: SupportsFoo
is a concept which checks that we can call get_foo
on t
with no arguments, and that the type of that expression is int
. The first calculate_foo_factor
will be selected by overload resolution for types which satisfy the SupportsFoo
concept, and the second will be chosen for those which don’t.
Unfortunately, our library has to support C++14. We’ll need to try something different. I’ll demonstrate a bunch of possible solutions to this problem in the next section. Some of them may seem complex if you aren’t familiar with the metaprogramming techniques used, but for now, just note the differences in complexity and abstraction between them. The metaprogramming tricks will all be explained in the following section.
Solutions
Here’s a possible solution using expression SFINAE:
Well, it works, but it’s not exactly clear. What’s the int
and ...
there for? Why do we need an extra overload? The answers are not the important part here; what is important is that unless you’ve got a reasonable amount of metaprogramming experience, it’s unlikely you’ll be able to write this code offhand, or even copy-paste it without error.
The code could be improved by abstracting out the check for the presence of the member function into its own metafunction:
Again, some more expert-only template trickery which I’ll explain later. Using this trait, we can use std::enable_if
to enable and disable the overloads as required:
This works, but you’d need to understand how to implement supports_foo
, and you’d need to repeat all the boilerplate again if you needed to write a supports_bar
trait. It would be better if we could completely abstract away the mechanism for detecting the member function and just focus on what we want to detect. This is what the detection idiom provides.
is_detected
here is part of the detection idiom. Read it as “is it valid to instantiate foo_t
with T
?” std::declval<T>()
essentially means “pretend that I have a value of type T
” (more on this later). This still requires some metaprogramming magic, but it’s a whole lot simpler than the previous examples. With this technique we can also easily check for the validity of arbitrary expressions:
We can also compose traits using std::conjunction
:
If you want to use is_detected
today, then you can check if your standard library supports std::experimental::is_detected
. If not, you can use the implementation from cppreference or the one which we will go on to write in the next section. If you aren’t interested in how this is written, then turn back, for here be metaprogramming dragons.
Metaprogramming demystified
I’ll now work backwards through the metaprogramming techniques used, leading up to implementing is_detected
.
Type traits and _v
and _t
suffixes
A type trait is some template which can be used to get information about characteristics of a type. For example, you can find out if some type is an arithmetic type using std::is_arithmetic
:
Type traits either “return” types with a ::type
member alias, or values with a ::value
alias. _t
and _v
suffixes are shorthand for these respectively. So std::is_arithmetic_v<T>
is the same as std::is_arithmetic<T>::value
, std::add_pointer_t<T>
is the same as typename std::add_pointer<T>::type
1.
decltype
specifiers
decltype
gives you access to the type of an entity or expression. For example, with int i;
, decltype(i)
is int
.
std::declval
trickery
std::declval
is a template function which helps create values inside decltype
specifiers. std::declval<foo>()
essentially means “pretend I have some value of type foo
”. This is needed because the types you want to inspect inside decltype
specifiers may not be default-constructible. For example:
SFINAE and std::enable_if
SFINAE stands for Substitution Failure Is Not An Error. Due to this rule, some constructs which are usually hard compiler errors just cause a function template to be ignored by overload resolution. std::enable_if
is a way to gain easy access to this. It takes a boolean template argument and either contains or does not contain a ::type
member alias depending on the value of that boolean. This allows you to do things like this:
If T
is integral, then the second overload will be SFINAEd out, so the first is the only candidate. If T
is floating point, then the reverse is true.
Expression SFINAE
Expression SFINAE is a special form of SFINAE which applies to arbitrary expressions. This is what allowed this example from above to work:
The first overload will be SFINAEd out if calling get_foo
on an instance of T
is invalid. The difficulty here is that both overloads will be valid if the get_foo
call is valid. For this reason, we add some dummy parameters to the overloads (int
and ...
) to specify an order for overload resolution to follow2.
void_t
magic
void_t
is a C++17 feature (although it’s implementable in C++11) which makes writing traits and using expression SFINAE a bit easier. The implementation is deceptively simple3:
You can see this used in this example which we used above:
The relevant parts are the class=void
and void_t<...>
. If the expression inside void_t
is invalid, then that specialization of supports_foo
will be SFINAEd out, so the primary template will be used. Otherwise, the whole expression will be evaluated to void
due to the void_t
and this will match the =void
default argument to the template. This gives a pretty succinct way to check arbitrary expressions.
That covers all of the ingredients we need to implement is_detected
.
The implementation
For sake of simplicity I’ll just implement is_detected
rather than all of the entities which the Lib Fundamentals 2 TS provides.
Let’s start with that last alias template. We take a variadic template template parameter (yes really) called Trait
and any number of other type template arguments. We forward these to our detail::is_detected
helper with an extra void
argument which will act as the class=void
construct from the previous section on void_t
4. We then have a primary template which will “return” false as the result of the trait. The magic then happens in the following partial specialization. Trait<Args...>>
is evaluated inside void_t
. If instantiating Traits
with Args...
is invalid, then the partial specialization will be SFINAEd out, and if it’s valid, it’ll evaluate to void
due to the void_t
. This successfully abstracts away the implementation of the detection and allows us to focus on what we want to detect.
That covers it for the detection idiom. This is a handy utility for clearing up some hairy metaprogramming, and is even more useful since it can be implemented in old versions of the standard. Let me know if there are any other parts of the code you’d like explained down in the comments or on Twitter. If you want some more resources on the detection idiom, you might want to watch the conference talks given by Walter Brown5 6, Marshall Clow7 8, and Isabella Muerte9.
-
See here for information on why the
typename
keyword is needed in some places. ↩ -
Due to standards defect CWG1558, implementations needed an extra level of abstraction to work on some compilers. ↩
-
The reason we can’t use the same trick is that parameter packs must appear at the end of the template parameter list, so we need to put the
void
in the middle. ↩ -
CppCon 2014: Walter E. Brown “Modern Template Metaprogramming: A Compendium, Part I” ↩
-
CppCon 2014: Walter E. Brown “Modern Template Metaprogramming: A Compendium, Part II” ↩
-
ACCU 2017: The Detection Idiom - a simpler way to SFINAE - Marshall Clow ↩
-
C++Now 2017: Marshall Clow “The ‘Detection idiom:’ A Better Way to SFINAE” ↩
-
CppCon 2016: Isabella Muerte “No Concepts Beyond This Point: The Detection Idiom” ↩
Let me know what you think of this article on twitter @TartanLlama or leave a comment below!