Logo

Blog


How C++20 Concepts can simplify your code

Today I like to talk about C++20's Concepts and how they help you simplify your code. Plus, make it more correct.

Consider a class template in which we like to disable a certain method.

1
2
3
4
5
6
template<typename T, bool enable = true>
class Sample
{
  public:
  int DisableThisMethodOnRequest() { return 42; }
};

The usual way to do this is to use SFINAE together with an enable_if.

1
2
3
4
5
6
template<typename T, bool enable = true>
class Sample
{
  public:
  std::enable_if_t<enable, int> DisableThisMethodOnRequest() { return 42; }
};

Well, the enable_if does not absolutely suit my eyes, but it looks concise and readable. Sadly, this code does not do what we want. SFINAE does not apply here, as we apply it to the method, not the class. To make this work we have to make DisableThisMethodOnRequest a template itself:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template<typename T, bool enable = true>
class Sample
{
  public:
  template<typename Dummy = void>
  std::enable_if_t<enable, int> DisableThisMethodOnRequest()
  {
    return 42;
  }
};

Note, in an earlier version this example was the same as the one before which was due to a copy and paste error. Thank you @MarkusWerle for spotting & reporting it!

To spare users to supply this template parameter, we set it to void as default and give it an ugly name. Hopefully, this implies don't use this template-parameter to all users. Well, for starters, that may not be that clear. Plus, we made this method a template which could raise questions. Now, we are shortly before having C++20 on our hands. Currently, ISO is evaluating the supposed to be final document. The design is closed so we can safely assume that Concepts will be in C++20 and as specified in the latest working draft.

Let's use C++20's Concepts

With Concepts on our hands we can rewrite the former example to this form:

1
2
3
4
5
6
template<typename T, bool enable = true>
class Sample
{
  public:
  int DisableThisMethodOnRequest() requires(enable) { return 42; }
};

We can get rid of the enable_if and the method no longer needs to be declared as a template. By simply putting a trailing requires clause after the function declaration we achieve the same outcome.

Except this time it is sharp, short and clear. I’m personally not in favour of these dummy default parameters. While using IDEs which give information about types or methods, they are visible to users, but they in fact are nothing a user should see or worry about.

The three valid places of requires

The requires-clause itself can appear in three places:

As the requires clause:

1
2
3
template<typename T>
requires YourRequirementOrConcept<T>
void func();

As the trailing-requires-clause:

1
2
template<typename T>
void func() requires YourRequirementOrConcept<T>;

This is the version we used in our example. However, here you see that it is also combinable with a template and a template-parameter.

As a constrained template parameter:

1
2
template<YourRequirementOrConcept T>
void func();

Here, the requires-clause is embedded in the Concept YourRequirementOrConcept. This is probably the most common form, if you have a requirement which is used multiple times. It gives you the ability to provide a proper name for it and have only a single implementation. The Concept here could look like this:

1
2
template<typename T>
concept YourRequirementOrConcept = requires SomeThing;

There is more

At times we like to provide a default constructor only dependent on some criteria. The example would be a wrapper type which should emulate the behavior of the wrapped type. This is a job for enable_if. Here is an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <type_traits>

template<bool WithDefaultCtor>
class ClassWithOptionalDefaultCtor
{
  public:
  template<typename std::enable_if<WithDefaultCtor, int>::type = 0>
  ClassWithOptionalDefaultCtor()  1  we cannot say =default here
  {
  }
};

int main()
{
  ClassWithOptionalDefaultCtor<true> t{};
}

Notice that at 1 we cannot use =default because this is not a default constructor. It is a method template looking like a default constructor. Such a thing cannot be defaulted as the compiler does not know what it is. The very poor solution shown above leads to uninitialized variables, if we do not add them to the constructors initializer list. Assuming that we're talking about a wrapper, it probably has only one member value. Adding this member isn't troublesome. On the other hand, this solution doesn't appear to be very generic.

C++20 lets us rewrite the code from before into this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
template<bool WithDefaultCtor>
class ClassWithOptionalDefaultCtor
{
  public:
  ClassWithOptionalDefaultCtor() requires(WithDefaultCtor) =
      default;  1  way better
};

int main()
{
  ClassWithOptionalDefaultCtor<true> t{};
}

That is truly beautiful code! Please notice at first, that the type_traits include is gone, as the ugly enable_if. That alone brings you a compile-time speedup. Whether it is noticeable is a different question. It also might go away, if the requires-condition gets more complex and requires concepts.

As in the examples before, I think the requires-clause makes the code much more readable. But the best part is that we now can apply =default! With C++20's Concepts we can have a true default constructor here.

Andreas