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.
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
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.
Here’s a possible solution using expression SFINAE:
Well, it works, but it’s not exactly clear. What’s the
... 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
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
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.
I’ll now work backwards through the metaprogramming techniques used, leading up to implementing
Type traits and
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
Type traits either “return” types with a
::type member alias, or values with a
_v suffixes are shorthand for these respectively. So
std::is_arithmetic_v<T> is the same as
std::add_pointer_t<T> is the same as
decltype gives you access to the type of an entity or expression. For example, with
std::declval is a template function which helps create values inside
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 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:
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 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 (
...) to specify an order for overload resolution to follow2.
You can see this used in this example which we used above:
The relevant parts are the
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
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_t4. 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
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.
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
voidin the middle. ↩
Let me know what you think of this article on twitter @TartanLlama or leave a comment below!