Cover Story

C++ Evolution Through the Lens of the Detection Idiom

Illustrated by Shiho Pate

The C++ language evolves quickly and it's difficult to keep up with sometimes. The same thing happens with the standard template library. However, sometimes one wonders: How fast is it really changing?

I'll try to give you a grasp from the point of view of one of the most beloved and hated tools of all time, the detection idiom. This idiom has undergone many changes over time and has transformed itself from an ugly duckling to a splendid swan. More than other idioms, its story gives us a clear picture of how much the language has changed without us even noticing it.

Introduction

Most likely, you've already come across the detection idiom. It doesn't matter whether you are a newbie or a seasoned programmer; you can find it everywhere and in many forms. For those who don't know what I'm talking about, I'll try to summarize it briefly.

The basic idea is to set a constraint on a template parameter or to select a template specialization (be it a class or a method) based on the fact that the type (or one of the types) provided has or doesn't have a given data member or member function. A typical case may be the following: if the type T has the method f, then invoke it; otherwise, do something else or just stop the compilation process. This is one of the cases where the detection idiom takes part in the game. In fact, as its name implies, it's used to literally detect whether a certain member of a class exists or not.

This idiom is the basic brick used to define requirements on types or to define a compile-time if. Unfortunately, this compile-time if doesn't exist yet, so it will be a bit more complicated to use than a simple if.

Pre-Modern C++

The so-called modern C++ started with the C++11. Therefore, we can consider everything that precedes it pre-modern C++. This revision of the language has held the stage for a long time, but all in all, it is rather poor when compared to the C++ we know today. In particular, there was no decltype yet, there were no utilities like std::enable_if_t, and std::declval was not born yet. In those days, the detection idiom was a mixture of tricks that exploited some
of the lesser-known features of the language. The good news, though, is that an if statement was already there:

template <typename Type>
Type declval();

template<typename Type>
struct detect_x {
  typedef char yes[1];
  typedef char no[2];

  template <typename Other>
  static yes & check(int(*)[sizeof(declval<Other>().x, 1)]);

  template <typename Other>
  static no & check(...);

  enum { value = (sizeof(check<Type>(NULL)) == sizeof(yes)) };
};

If it looks scary, it's because it is scary, especially for those who have never worked with C++98 or similar. However, it turns out that it's not so scary. This simple implementation detects if a type has a data member named x. If you remember from my previous article, the sizeof operands are unevaluated and we can use this operator to SFINAE the check function. The function parameter pack allows us to define a fallback instead, and the return type of check (or rather its sizeof) will tell us if our test was successful or not.

We can now use it as:

template<bool, typename>
struct enable_if {};

template<typename Type>
struct enable_if<true, Type> { typedef Type type; };

template<typename Type>
enable_if<detect_x<Type>::value, void>::type f() {
    // use Type::x
}

template<typename Type>
enable_if<!detect_x<Type>::value, void>::type f() {
    // no Type::x here
}

Of course, we also have to define our version of std::enable_if_t because it didn't exist before. Finally, we have our compile-time if! But what an effort...

Modern C++

Then came C++11, and things evolved for the better. This revision of the language has brought with it some tools that have radically changed the way we program in C++. The detection idiom was finally ready for a review...and we also had std::enable_if_t in the standard template library!

C++11 made it common to define a detector both as a one-off or a reusable solution. It's enough to say that in C++98 you needed to define a class for each member you wanted to probe and the only alternative was to get lost in a macro hell. The one-off version of our tool was based on decltype, one of the most important specifiers introduced by the language:

template<typename Type>
auto f(int) -> decltype(&Type::x, void()) {
    // use Type::x
}

template<typename Type>
void f(char) {
    // no Type::x here
}

template<typename Type>
void f() {
    return f<Type>(0);
}

This is a simplified version of the choice trick. We exploit the overloading of the function f to test our type and, only in the case in which it doesn't have the data member x, we choose the fallback. Again SFINAE, it's still a template, but in a few lines we have wiped out years of suffering. Not bad.

The reusable version is slightly more verbose than the one above but light years better than its pre-modern C++ counterpart:

template<typename, typename = void>
struct has_x: std::false_type {};

template<typename Type>
struct has_x<Type, decltype(&Type::x, void())>: std::true_type {};

template<typename Type>
std::enable_if_t<has_x<Type>::value> f() {
    // use Type::x
}

template<typename Type>
std::enable_if_t<!has_x<Type>::value> f() {
    // no Type::x here
}

We cannot escape from having to define a detector for each member we want to probe, but the amount of code needed to do so is definitely more reasonable now. However, our compile-time if isn't yet an if. This is a fact.

C++17 Means if constexpr

The detection idiom hasn't changed much with C++17, but the way we interact with it changed quite a lot actually. At least it did in many cases. There is now much less SFINAE around and we finally have a real compile-time if, the so called constexpr-if:

template<typename Type>
void f() {
    if constexpr(has_x<Type>::value) {
        // use Type::x
    } else {
        // no Type::x here
    }
}

We can say, without risk of seeming too fond of it, that the detection idiom has finally found a home. The C++17 seemed to be the icing on the cake with regard to the evolution over the years of our favorite idiom. It even had a comfortable place to express itself: a compile-time if. The logic is contained in a single function and we can easily spot what we are going to do when we have an x-friendly type and what we will do when the type has no x data members at all.

What could go wrong?

Then Came Concepts

Just when everything seemed to make sense, C++20 arrived and brought the concepts with it. The definition of this new tool says a lot about our idiom's future: things are almost as simple as it gets, so the next version might be the last:

A concept is a named set of requirements.

Wait a minute. What else is our detection idiom if not a named set of requirements? We've seen it in fairly simple forms so far, but nothing prevents us from defining a detector that probes a type in search of a data member x and a function y. We can set all the constraints we want, in fact.

Let's take a step back though: the concepts.

How will they work? This is an example of what lies ahead:

template<typename Type>
requires has_x<Type>::value
void f(int) {
    // use Type::x
}

template<typename Type>
void f(char) {
    // no Type::x here
}

Actually, this isn't the best example. First of all, we are defining concepts inline (see the requires clause above) as if they were one-off requirements to throw away immediately after use. Moreover, we are using concepts to do SFINAE, and this isn't exactly their intended purpose. A more refined example of using a properly defined concept the way it was intended could be the following:

template <class Type>
concept XFriendly = requires(Type type) { type.x; };

template<XFriendly Type>
void f() {
    // use Type::x
}

Here the function f requires its argument to be an x-friendly type, that is, a type that has a x data member to use. A more explicit, though more verbose, version would be:

template<typename Type>
requires XFriendly<Type>
void f() {
    // use Type::x
}

In both cases, we have an if rather than an if-else statement at compile-time. Its pre-C++20 counterpart would be this:

template<typename Type>
void f() {
    static_assert(has_x<Type>::value;
    // use Type::x
}

Of course, concepts are such a wide field to explore that we cannot fully discuss them in detail in a section of an article that is focused on a completely different topic. That said, this brief mention should give you an idea of how the tool makes the focus on the requirements shift from the function declaration to the concept itself.

Both decltype and std::enable_if_t get the job done as well as a plain static_assert but they also heavily pollute the function declaration and make it hard to spot where and how something happens, what the constraints on a type are, and so on. On the other hand, if constexpr can quickly become ugly if you have many constraints to put on a type. The other solution is to define a _bigger_constraint that is the sum of simpler requirements, but...isn't it what one would already do with concepts? If it becomes only a matter of syntax and the language helps us with nice keywords, why should I use something homemade?

All in all, concepts, and thus C++20, didn't stop our journey in the language. Instead, they add more value and make it possible to use our preferred idiom in a more appealing manner. If a compile-time if seemed like the final touch on the topic, concepts are an unexpected step forward. They not only make it possible to detect if a type has a trait we need in its API, but they also make the code we use to do that easy to read, understand, and maintain, with a clear separation between the functions and the requirements for our types.

C++ is changing for the better every few years, adding some major features and a few lesser-known tools with every release. We barely remember the ugly combinations of templates and arcane techniques we were used to back in the day to do what nowadays can be achieved with a few elegant lines of code. The detection idiom went through all these changes and evolved more than others, one revision at a time. Sometimes it went toward a more compact version, and additional revisions have resulted in a more elegant solution. It has finally a home. It's there, but it's also hidden behind a concept now. It's defined with a bunch of lines of code, and it's pretty easy to generalize. More important than everything else, though, is it continues to get the job done!

We cannot know what the future holds for our preferred language, but someone is doing it right in order to make C++ nicer to use, easier to read, and, sometimes, even less prone to abuses.

Michele Caini

author

Michele is fond of two things: C++ and gaming. When he isn't spending his time attending conferences, he blogs about coding and works on his popular open source game engine EnTT.

Shiho Pate

illustrator

California based Illustrator Shiho Pate has been in the games industry for 10+ years. Now she is shifting her focus on editorial and children's book Illustrations. For more information please visit www.shihopate.com and follow her on twitter and instagram @shihopate