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 |
|
The usual way to do this is to use SFINAE together with an enable_if
.
1 2 3 4 5 6 |
|
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 |
|
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 |
|
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 |
|
As the trailing-requires
-clause:
1 2 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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