Programming with C 20 Concepts Coroutines Ranges and more 1st Edition Andreas Fertig download
Programming with C 20 Concepts Coroutines Ranges and more 1st Edition Andreas Fertig download
https://ebookmeta.com/product/programming-with-c-20-concepts-
coroutines-ranges-and-more-1st-edition-andreas-fertig/
https://ebookmeta.com/product/pro-cryptography-and-cryptanalysis-
with-c-20-creating-and-programming-advanced-algorithms-1st-
edition-marius-iulian-mihailescu/
https://ebookmeta.com/product/pro-cryptography-and-cryptanalysis-
with-c-20-creating-and-programming-advanced-algorithms-1st-
edition-marius-iulian-mihailescu-2/
https://ebookmeta.com/product/functional-programming-with-c-
create-more-supportable-robust-and-testable-code-second-early-
release-simon-j-painter/
https://ebookmeta.com/product/media-in-process-transformation-
and-democratic-transition-1st-edition-sai-felicia-krishna-hensel-
editor/
The House on Woody Creek Lane 1st Edition Claudine
Marcin
https://ebookmeta.com/product/the-house-on-woody-creek-lane-1st-
edition-claudine-marcin/
https://ebookmeta.com/product/fantastic-schools-christopher-g-
nuttall-et-el/
https://ebookmeta.com/product/ready-for-first-coursebook-with-
key-3rd-edition-roy-norris/
https://ebookmeta.com/product/constituent-power-law-popular-rule-
and-politics-1st-edition-matilda-arvidsson-editor/
https://ebookmeta.com/product/culture-and-politics-perez-zagorin/
The Labour of Laziness in Twentieth Century American
Literature 1st Edition Zuzanna Ladyga
https://ebookmeta.com/product/the-labour-of-laziness-in-
twentieth-century-american-literature-1st-edition-zuzanna-ladyga/
Programming
with C++20
Concepts, Coroutines,
Ranges, and more
Andreas Fertig
Andreas Fertig
1. Edition
© 2021 Andreas Fertig
https://AndreasFertig.Info
All rights reserved
The work including all its parts is protected by copyright. Any use outside the limits of the copyright law
requires the prior consent of the author. This applies in particular to copying, editing, translating and
saving and processing in electronic systems.
The reproduction of common names, trade names, trade names, etc. in this work does not justify
the assumption that such names are to be regarded as free within the meaning of the trademark
and trademark protection legislation and therefore may be used by everyone, even without special
identification.
Published by:
Fertig Publications
https://andreasfertig.info
ISBN: 978-3-949323-01-0
This book exists to assist you during your daily job life or hobbies. All examples in
this book are released under the MIT license.
The main reason for the MIT license was to avoid uncertainty. It is a well estab-
lished open source license and comes without many restrictions. That should make
it easy to use it even in closed-source projects. In case you need a dedicated license
or have some questions around it, feel free to contact me.
Code download
Used Compilers
For those of you who try to test the code and like to know the compiler and revision
I used here you go:
■ g++ 11.1.0
Andreas Fertig, CEO of Unique Code GmbH, is an experienced trainer and lecturer
for C++ for standards 11 to 20.
Andreas is involved in the C++ standardization committee, in which the new stan-
dards are developed. At international conferences, he presents how code can be writ-
ten better. He publishes specialist articles, e.g., for iX magazine, and has published
several textbooks on C++.
With C++ Insights (https://cppinsights.io), Andreas has created an internationally
recognized tool that enables users to look behind the scenes of C++ and thus to un-
derstand constructs even better.
Before working as a trainer and consultant, he worked for Philips Medizin Sys-
teme GmbH for ten years as a C++ software developer and architect focusing on
embedded systems.
You can find him online at andreasfertig.info and his blog at andreasfertig.blog
8
About the Book
Programming with C++20 teaches programmers with C++ experience the new fea-
tures of C++20 and how to apply them. It does so by assuming C++11 knowledge.
Elements of the standards between C++11 and C++20 will be briefly introduced, if
necessary. However, the focus is on teaching the features of C++20.
You will start with learning about the so-called big four Concepts, Coroutines,
std::ranges, and modules. The big four a followed by smaller yet not less important
features. You will learn about std::format, the new way to format a string in C++.
In chapter 6, you will learn about a new operator, the so-called spaceship operator,
which makes you write less code.
You then will look at various improvements of the language, ensuring more con-
sistency and reducing surprises. You will learn how lambdas improved in C++20 and
what new elements you can now pass as non-type template parameters. Your next
stop is the improvements to the STL.
Of course, you will not end this book without learning about what happened in
the constexpr-world.
The following shows the execution of a program. I used the Linux way here and
skipped supplying the desired output name, resulting in a.out as the program name.
$ ./a.out
Output
From time to time, I use an element from a previous standard after C++11. I ex-
plain these elements in dedicated standard boxes such as the following:
0.1 C++14: A sample element
These boxes carry the standard in which this element was introduced and are num-
bered such that I can reference them like this: Std-Box 0.1.
All listings are numbered and sometimes come with annotations which I refer to
like this A .
References carry a page number in case the reference isn’t on the same page. For
example, Std-Box 0.1 has no page number because it appears on the same page.
Feedback
Thank you
I like to say thank you to everyone who reviewed drafts for this book and gave me
valuable feedback. Thank you! All this feedback helped to improve the book. Here
is a list of people who provided feedback: Vladimir Krivopalov, Hristiyan Nevelinov,
John Plaice, Peter Sommerlad, Salim Pamukcu, and others.
About the Book 11
Revision History
Acronyms 327
Bibliography 329
Index 331
Chapter 1
Concepts:
Predicates for strongly typed
generic code
Templates have been with C++ since the early beginnings. Recent standard updates
have added new facilities, such as variadic template. Templates enable Generic Pro-
gramming (GP), the idea of abstracting concrete algorithms to get generic algorithms.
They can then be combined and used with different types to produce a wide variety
of software without providing a dedicated algorithm for each type. GP or Template
Meta-Programming (TMP) are powerful tools. For example, the Standard Template
Library (STL) heavily uses them.
However, template code has always been a bit, well, clumsy. When we write a
generic function, we only need to write the function once; then it can be applied to
various different types. Sadly, when using the template with an unsupported type,
finding any error requires a good understanding of the compiler’s error message.
All we needed to face such a compiler error message was a missing operator< that
wasn’t defined for the type. The issue was that we had no way of specifying the re-
quirements to prevent the misuse and at the same time give a clear error message.
18
This chapter comes with an additional challenge. The language feature we will discuss in this
chapter is called Concepts. We can also define a concept ourselves, and there is a concept key-
word. When I refer to the feature itself, it is spelled with a capital C, Concepts. The lower case
version is used when I refer to a single concept definition, and the code-font version concept
refers to the keyword.
Let’s consider a simple generic Add function. This function should be able to add an
arbitrary number of values passed to it and return the result. Much like this:
1 const int x = Add(2,3,4,5);
2 const int y = Add(2,3);
3 const int z = Add(2, 3.0); A This should not compile
While the first two calls to Add, x and y, are fine, the third call should result in a
compile error. With A we are looking at implicit conversions, namely a promotion
from int to double because of 3.0. Implicit conversions can be a good thing, but in
this case, I prefer explicitness over the risk of loss of precision. Here Add should only
accept an arbitrary number of values of the same data type.
To make the implementation a little more challenging, let’s say that we don’t want
a static_assert in Add, which checks that all parameters are of the same type. We
would like to have the option of providing an overload to Add that could handle cer-
tain cases of integer promotions.
To see the power of Concepts, we start with an implementation in C++17. For
the implementation of Add, we obviously need a variadic function template as well
as a couple of helpers. The implementation I present here requires two helpers,
are_same_v and first_arg_t. You can see the implementation in Listing 1.1.
5 template<typename T, typename...>
6 struct first_arg {
7 using type = T;
Listing 1.1
8 };
9
10 template<typename... Args>
11 using first_arg_t = typename first_arg<Args...>::type;
Variable templates were introduced with C++14. They allow us to define a variable, which is a
template. This feature allows us to have generic constants like π :
Listing 1.2
1 template<typename T>
2 constexpr T pi(3.14);
One other use case is to make TMP more readable. Whenever we had a type-trait that had a value
we wanted to access, before C++14, we needed to do this: std::is_same<T, int>::value
. Admittingly, the ::value part was not very appealing. Variable templates allow the value of
::value to be stored in a variable.
Listing 1.3
1 template<typename T, typename U>
2 constexpr bool is_same_v = std::is_same<T, U>::value;
With that, the shorter and more readable version is is_same_v<T, int>. Whenever you see a
_v together with a type-trait, you’re looking at a variable template.
Our second helper, first_arg_t, uses a similar trick. It extracts the first type
from a pack and stores thing one in a using-alias. That way, we have access to the
first data type in a parameter pack, and since we later ensure that all types in the
pack are the same, this first type is as good as that from any other index choice in the
parameter pack.
20
Great, now that we have our helpers in place, let’s implement Add. Listing 1.4
provides an implementation using C++17.
1 template<typename... Args>
2 std::enable_if_t<are_same_v<Args...>, first_arg_t<Args...>>
Listing 1.4
3 Add(const Args&... args) noexcept
4 {
5 return (... + args);
6 }
Before C++17, whenever we had a parameter pack, we needed to recursively call the function
which received the pack and split up the first parameter. That way, we could traverse a parameter
pack. C++17 allows us to apply an operation to all elements in the pack. For example, int
result = (... + args); applies the + operation to all elements in the pack. Assuming that
the pack consists of three objects, this example will produce int result = arg0 + arg1 +
arg2;. This is much shorter to write than the recursive version. That one needs to be terminated
at some point. With fold expressions, this is done automatically by the compiler. We can use other
operations instead of +, like −, /, ∗, and so on.
The important thing to realize about fold expressions is that it is only a fold expression if the pack
expansion has parentheses around it and an operator like +.
So far, I hope that’s all understandable. The part I heavily object to, despite that
it is my own code, is the line with the enable_if_t. Yes, it is the state-of-the-art
enable_if because, with the _t, we don’t need to say typename in front of it. How-
ever, this single line is very hard to read and understand. Depending on your knowl-
edge of C++, it can be easy, but remember the days when you started with C++. There
is a lot that one has to learn to understand this single line.
The first part, or argument, is the condition. Here we pass are_same_v. Should
this condition be true, the next parameter gets enabled, which is first_arg_t. This
then becomes the return type of Add. Right, did you also miss the return type initially?
Chapter 1: Concepts: Predicates for strongly typed generic code 21
Should the condition be false, then this entire expression isn’t instantiable, we speak
of substitution failure is not an error (SFINAE) as the technique used here, and this
version of Add isn’t used for lookups by the compilers. The result is that we can end
up with page-long error messages where the compiler informs us about each and
every overload of Add it tried.
One more subtle thing is that in this case, enable_if does something slightly dif-
ferent than just enabling or disabling things. It tells us the requirements for this func-
tion. Yet, the name enable_if doesn’t give many clues about that.
All these things are reasons why people might find templates tremendously diffi-
cult to process. But, yes, I know, those who stayed accommodated to all these short-
comings.
Now it is time to see how things change with C++20.
Sticking with the initial example, we ignore the helpers, as they stay the same. List-
ing 1.5 presents the C++20 implementation of Add.
1 template<typename... Args>
2 A Requires-clause using are_same_v to ensure all Args are of the same
type.
Listing 1.5
3 requires are_same_v<Args...>
4 auto Add(Args&&... args) noexcept
5 {
6 return (... + args);
7 }
Here we can see that Add remains a variadic function template, probably not the
biggest surprise. Let’s again skip two lines and go to the definition of Add. What first
springs into our eyes is the return type. I chose auto. But the important thing is that
the return type is there! The rest of the function’s signature, as well as the function
body, are unchanged. I see this return type as the first win. Before the enable_if
obfuscated the return type.
22
The biggest improvement is the line that says requires. Isn’t that what’s really
going on here? This function Add requires that are_same_v is true. That’s all. I find
that pretty easy to read. The intent is clearly expressed without obfuscating anything
or requiring weird tricks. Okay, maybe we have to look up what are_same_v does,
but I can live with that.
We are looking at one of the building blocks of Concepts in Listing 1.5 on page 21,
the requires-clause.
Before we talk about how we can great Concepts are, let’s first see where we can apply
them. Figure 1.1 on page 23 lists all the places in a template declaration where we
can apply Concepts.
We see a type-constraint in C1. In this place, we can only use Concepts. We can use
a type-constraint instead of either class or typename in a template-head to state as
early as possible that this template takes a type deduced by the compiler, but it must
meet some requirements.
The next option is with C2, using a requires-clause. We already applied that in our
Add example in Listing 1.5 on page 21. In a requires-clause, we can use either con-
cepts or type-traits. The expression following the requires must return a boolean
value at compile time. If that value is true, the requirement(s) is (are) fulfilled.
The two places of C3 and C4 are similar. They both apply to placeholder types
constraining them. We can also use Concepts to constrain auto variables, which we
will see later. A constraint placeholder type works only with Concepts. Type-traits
are not allowed. In C4, we see something that you might already know from C++14’s
generic lambdas, Std-Box 7.1 on page 210, auto as a parameter type. Since C++20,
they are no longer limited to generic lambdas.
At the end, we have the trailing requires-clause. This one is similar to the requires-
clause. We can use Concepts or type-traits and can use boolean logic to combine
them. Table 1.1 on page 23 gives a guidance when to use which constraint form.
Chapter 1: Concepts: Predicates for strongly typed generic code 23
type-constraint
requires-clause
template<C1 T>
requires C2<T>
C3 auto Fun(C4 auto param) requires C5<T>
trailing requires-clause
constrained placeholder type
Figure 1.1: The different places where we can constrain a template or template argument.
C1 type-constraint Use this when you already know that a template type
parameter has a certain constraint. For example, not all
types are allowed. In Figure 1.1 the type is limited to a
floating-point type.
C2 requires-clause Use this when you need to add constraints for multiple
template type or non-type template parameters.
We’ve already seen the two forms of a requires-clause. It is time to look at our Add
example again and see what we can improve with the help of Concepts.
The current implementation of Add only prevents mixed types. Let’s call this re-
quirement A of Add. By that, the implementation leaves a lot unspecified:
B Add can nonsensically be called with only one parameter. The function’s name,
on the other hand, implies that things are added together. It would make more
sense if Add would also require to be called with at least two parameters. Ev-
erything else is a performance waste.
24
C The type used in Args must support the + operation. This is a very subtle re-
quirement that harshly yells at us once we violate it. It is also a design choice of
the implementor of Add. Instead of operator+, one could also require that the
type comes with a member function Addition. That would, of course, rule out
built-in types. Should we miss that, we again get these page-long errors that
are hard to see through. Only documentation helps at this point, and documen-
tation over time may disagree with the implementation. In such a case, I prefer
a check by the compiler over documentation.
D The operation + should be noexcept, since Add itself is noexcept. Did you
spot that initially? The implementation of Add in Listing 1.5 on page 21 and
before was always marked noexcept. Why? Because it mainly adds num-
bers, and I don’t want to have a try-catch-block around something like 3 + 4.
But since Add is a generic function, it also works with, for example, a std::
string, which can throw an exception. Writing a check for the noexceptness
of operator+ pre C++17 is an interesting exercise.
E The return type of operation + should match that of Args. Another interesting
and often overlooked requirement. It is surprising should operator+ of type
T return a type U. Sometimes, there are good reasons for such a behavior, but
is doesn’t seem plausible in the case of Add. Let’s restrict this as well.
requires(T t, U u)
{ Body of the
// some requirements
requires-expression
}
We are totally free when it comes to the qualifiers of these types. We can say that
a requires-expression takes a const T& or a const T*. Well, we can even start en-
tering the east and west const debate. Shaping these parameters helps us later when
we refer to them in the body of a requires-expression which we will see in §1.12.1 on
page 40.
Next in a requires-expression is the body, like with a function. This body comes
with a requirement itself. It must contain at least one requirement.
The difference between a requiresclause and a requiresexpression
As the name implies, a simple requirement checks for a simple thing, namely whether
a certain statement is valid. For the Add example, Listing 1.6 illustrates a simple
requirement that checks whether the fold expression used in the body of Add is valid.
1 requires(Args... args)
Listing 1.6
2 {
3 (... + args); C SR: args provides +
4 }
This check ensures that the type passed to Add provides operator+. We just have
to check our first requirement C .
1 requires(Args... args)
2 {
3 (... + args); C SR: args provides +
Listing 1.7
The two last requirements for the function Add can be checked with a compound
requirement. We can check two things with a compound requirement, the return
type of an expression and the noexceptness of that expression. Listing 1.8 shows the
compound requirement for the requirements D and E of Add.
1 requires(Args... args)
2 {
3 (... + args); C SR: args provides +
4 requires are_same_v<Args...>; A NR: All types are the same
5 requires sizeof...(Args) > 1; B NR: Pack contains at least two
elements
Listing 1.8
7 D E CR: ...+args is noexcept and the return type is the same as the
first argument type
8 // { (... + args) } noexcept;
9 // { (... + args) } -> same_as<first_arg_t<Args...>>;
10 { (... + args) } noexcept -> same_as<first_arg_t<Args...>>;
11 }
28
After noexcept, we see a token sequence that looks like a trailing return type, and
we can read it as such. This trailing return type-like arrow is followed by a concept.
At this point, we must use a concept. Type-traits won’t work at this place. The con-
Chapter 1: Concepts: Predicates for strongly typed generic code 29
cept you can see is same_as from the STL. Its purpose is to compare two types and
check whether they are the same. If you look at Listing 1.8 on page 27 closely, you
can see that I pass only one argument to same_as, the resulting type of first_arg_t.
So, where is the second parameter? The answer is, the compiler injects as the first
parameter the resulting type from the compound statement at the beginning of the
line. Pretty handy, right?
Basically, the form of the compound requirement as presented here does two
checks in one. It checks the noexcept state of the expression and the resulting
type. We can split this into two steps and check for noexcept, simply but omitting
everything after noexcept. Then we can do the return type check in the second
check by striking noexcept from the line as presented. I prefer having both checks
in a single statement.
Table 1.2 on page 28 captures all four kinds of requires-expression s in a compact
manner.
Fine, at this point, we have created a requires expression that checks all the re-
quirements of Add we established in §1.4 on page 23. Next, we look at how to attach
this requires expression function Add.
The last variant of requirement we can have in a requires-expression is the type re-
quirement. This type of requirement asserts that a certain type is valid. Listing 1.9
defines a concept containerTypes that checks that a given type T provides all the
types that allocating containers of the STL in C++ usually provide.
1 template<typename T>
2 concept containerTypes = requires(T t)
3 { A Testing for various types in T
4 typename T::value_type;
Listing 1.9
5 typename T::size_type;
6 typename T::allocator_type;
7 typename T::iterator;
8 typename T::const_iterator;
9 };
30
Here you can see that a type requirement always starts with typename. Should we
leave the typename out, we are back at a simple requirement.
The easiest form is to attach the requires expression built in §1.5 on page 25 to the
requires clause of Add, as Listing 1.10 shows.
1 template<typename... Args>
2 requires requires(Args... args)
3 {
4 (... + args);
5 requires are_same_v<Args...>;
Listing 1.10
6 requires sizeof...(Args) > 1;
7 { (... + args) } noexcept -> same_as<first_arg_t<Args...>>;
8 }
9 auto Add(Args&&... args)
10 {
11 return (... + args);
12 }
The grey part is the code we developed in §1.5 on page 25, the requires expres-
sion. You can see that the first line of the requires expression, where it starts with
requires, has a requires in front of it. So we have requires requires. What we
are looking at here is a so-called ad hoc constraint. The first requires introduced
the requires clause, C2 in Figure 1.1 on page 23, while the second starts the requires
expression. Instead of C2, we can, of course, also attach the requires expression to
the trailing requires clause, which is C5 in Figure 1.1 on page 23.
While an ad hoc constraint is handy, it is also the first sign of a code-smell. We
just spent some time developing the requires expression, but all we can do is to use
it in one place. Yes, I assume copy and paste is out of the question. The requires
expression for Add might be something special that only applies to Add. In that case, it
is fine to use it in an ad hoc constraint, but this isn’t true for most of the requirements.
Always think twice before using an ad hoc constraint.
Chapter 1: Concepts: Predicates for strongly typed generic code 31
template-head
How can we do better? Did you notice that I haven’t shown you Concepts yet?
Only building blocks to concepts and application areas. Now that we have a requires
expression, let’s start creating a Concept with it.
We continue with Add and use the requires expression from §1.5 on page 25 for
building our first concept. Figure 1.3 illustrates the components of a concept.
A concept always starts with a template-head. The reason is that concepts are pred-
icates in generic code, which makes them templates. The template-head of a concept
comes with the same power and limitations as that of any other function or class
template. We can use type and non-type template parameters (NTTPs) parameters,
class or typename, or a concept to declare a template type parameter.
After the template-head, Figure 1.3 shows the new keyword concept. This starts
the concept and tells the compiler that this is the definition of a concept and not, for
example, a variable template.
Of course, a concept must have a name. I picked MyConcept, yes I know, a great
name. After the concept name, we see the equal sign followed by requirements. We
assign these requirements to our concept MyConcept. As you can see, these require-
ments can be put together using boolean algebra. Figure 1.3 also shows that we can
use either concepts or type-traits as requirements.
32
With that knowledge, we can look at Listing 1.10 on page 30, which uses the
requires expression for Add and attaches it this time to a concept called Addable.
1 template<typename... Args>
2 concept Addable = requires(Args... args)
3 {
4 (... + args);
5 requires are_same_v<Args...>;
6 requires sizeof...(Args) > 1;
7 { (... + args) } noexcept -> same_as<first_arg_t<Args...>>;
Listing 1.11
8 };
9
10 template<typename... Args>
11 requires Addable<Args...>
12 auto Add(Args&&... args)
13 {
14 return (... + args);
15 }
Once again, the grey part is the requires expression from §1.5 on page 25. Aside
from the concept Addable, Listing 1.11 shows how Add itself changes by using the
concept. We use Addable now in the requires clause of Add. This helps make Add
more readable, which I find a valuable improvement. The other part is that Addable
is now reusable. We use it with other functions then Add with a similar requirement.
So far, we have looked at the different requirement kinds, how to apply them, and
how they fit into a requires clause or to a concept. It is time to talk about how to
verify our requirements or concepts. As I previously said, we will spend a lot of time
in the future developing concepts, so we should also be able to test them like usual
code.
The good thing about testing concepts is that we already have all the necessities in
place. Remember, concepts only live at compile time. We have a tool to check things
at compile time with static_assert, no need to check out a testing framework.
Chapter 1: Concepts: Predicates for strongly typed generic code 33
We keep using Add or, better, the concept we created Addable. To test the vari-
ous combinations, a type must be valid for Addable (or, of course, invalid). I prefer
creating a stub that mocks the different types. Such a stub is shown in Listing 1.12.
Listing 1.12
10 int operator+(const Stub& rhs) noexcept(nexcept)
11 requires(operatorPlus && not validReturnType)
12 { return {}; }
13 };
14
Here Stub is a class template with three NTTPs A . The first one, nexcept, con-
trols whether the implementation of operator+ in Stub is noexcept. The second
parameter, operatorPlus, uses a trailing requires clause on operator+ in Stub
for enabling or disabling the operator. Last, validReturnType decides whether the
return type of operator+ is Stub, and by that, valid according to our requirements
for Addable, or int. The choice for int is arbitrary. All that’s needed is something
different than Stub.
At the bottom of Listing 1.12 at D , you can see more meaningful using aliases for
the different parameter combinations of Stub. For example, I cannot easily recall
what Stub<true, false, true> does, but I understand that ValidClass implies
that this type should be accepted by Addable.
34
Well, that’s it, at least the mocking part. With that we have everything we need to
start writing tests, which Listing 1.13 shows.
Listing 1.13
9 static_assert(Addable<ValidClass, ValidClass>);
10 static_assert(not Addable<NoAdd, NoAdd>);
11
Here static_assert is used to test all the various combinations of valid and in-
valid types for Addable. Fantastic, isn’t it? All in our favorite language, all without
macros, all without any external testing framework.
We have seen the three places where we can use a concept in a function template to
constrain a template parameter. C++20 opened the door for GP to look more like
regular programming. We can use a concept in the so-called abbreviated function
template syntax. This syntax comes without a template-head, making the function
terse. Instead, we declare what looks like a regular function, using the concept as a
parameter type, together with auto.
Chapter 1: Concepts: Predicates for strongly typed generic code 35
In the background, the compiler creates a function template for us. Each concept pa-
rameter becomes an individual template parameter, to which the associated concept
is applied as a constraint. This makes this syntax a shorthand for writing function
templates. The abbreviated function template syntax makes function templates less
scary and looks more like regular programming. The auto in such a function’s sig-
nature is a sign that we are looking at a template.
The abbreviated function template syntax can be a little terser. The constraining
concept is optional. We can indeed declare a function, with only auto parameters.
This way, C++20 allows us to create a function template in a very comprehensive
way.
The abbreviated syntax, together with Concepts, enables us to write less but more
precise code. Assume a system in which certain operations need to acquire a lock
before performing an operation. An example is a file system operation with multi-
ple processes trying to write data onto the file system. As only one can write at a
time because of control structures that have to be maintained, such a write opera-
tion is often locked by a mutex or a spinlock. Thanks to C++11’s lambdas, we can
write a DoLocked function template that takes a lambda as an argument. In its body,
DoLocked first acquires a global mutex globalOsMutex, using a std::lock_guard
to release the mutex after leaving the scope. Then in the next line, the lambda itself is
executed, safely locked without each user needing to know which mutex to use. Plus,
the scope is limited and, thanks to std::lock_guard, the mutex is automatically
released. Deadlocks should no longer happen.
1 template<typename T>
2 void DoLocked(T&& f)
3 {
Listing 1.14
4 std::lock_guard lck{globalOsMutex};
5
6 f();
7 }
36
I have used this pattern in different variations in many places, but there is one
thing that I have always disliked. Can you guess what? Correct, typename T. It is not
obvious to a user that DoLocked requires some kind of callable, a lambda, a function
object, or a function. Plus, for some reason, in this particular case, the template-head
added some boilerplate code I did not like.
With a combination of the new C++20 features, Concepts and abbreviated func-
tion templates, we can get rid of the entire template-head. As the parameter of this
function, we use the abbreviated syntax together with the concept std::invocable.
The function’s requirements are clearly visible now.
Listing 1.15
3 std::lock_guard lck{globalOsMutex};
4
5 f();
6 }
This is just one example showing how abbreviated templates reduce the code to
the necessary part. Thanks to Concepts, the type is limited as necessary. I often
claim that, especially for starters, this is much more understandable than the previous
version. Thinking of a bigger picture with a more complex example, this syntax is
useful for experts as well. Clarity is key here.
Even more terse
In earlier proposals of the Concepts feature, the abbreviated function template syntax was even
terser, without auto, using just the concept as type. However, some people claim that new fea-
tures must be expressive and type intensive before users later complain about all the overhead
they have to type. Maybe, in a future standard, we will be able to write the terse syntax, without
auto.
quirement in a Concept? The answer is yes, but only if the function does not use a
parameter created in a requires expression or if we play tricks. Sounds complicated,
right? It is. Listing 1.16 provides an example.
Listing 1.16
6
as NTTP to ensure that whatever container gets passed has a size of exactly 1. I hope
that SendOnePing being an abbreviated function template is not the most interesting
part here, but the way SizeCheck is called. The compiler is smart enough to deduce
the type of the function’s parameter and passes it as the first argument to the concept
SizeCheck. The second parameter is specified explicitly by us. Cool, right?
Listing 1.17 on page 38 shows that SendOnePing can be called with a std::array
of int with a size of one. No other size is allowed by the concept.
38
Listing 1.17
2
3 SendOnePing(a);
The trick we had to play is to make T of the concept a part of the constexpr
function used inside the concept. We cannot use
1 requires(T t) { requires t.size() == N; };
For some years now, at least some of us struggled with placeholder variables with a
definition of the form auto x = something;. One argument I often hear against
using auto instead of a concrete type is that with this syntax, it is hard to know the
variable’s type without a proper Integrated Development Environment (IDE). I agree
with that. Some people at this point tell me and others to use a proper IDE, problem
solved. However, in my experience, this is not entirely solving the problem. Think
about code review, for example. They often take place in either a browser showing
the differences or another diff tool, tending not to provide context. All that said,
there is also a good argument for using auto variables. They help us get the type
right, preventing implicit conversion or loss of precision. Herb Sutter showed years
ago [1] that in a lot of cases, we could put the type at the right, in doing so leave clues.
Let’s look at an example where auto makes our code correct.
Listing 1.18
1 auto v = std::vector<int>{3, 4, 5};
2 const int size = v.size(); A int is the wrong type
This example uses int to store the size. The code compiles and, in a lot of cases,
works. Despite that, the code is incorrect. A std::vector does not return int in its
size function. The size function usually returns size_t, but this is not guaranteed.
Because of that, there is a size_type definition in the vector that tells us the correct
type. Especially if your code runs on different targets using different compilers and
standard libraries, these little things matter. The correct version of the code is using
the size_type instead of int. To access it, we need to spell out the vector, including
its arguments, making the statement long and probably beginner unfriendly.
Listing 1.19
3 A Using the
correct type
4 const std::vector<int>::size_type size = v.size();
The alternative so far was to use auto, as shown below, and by that, let the com-
piler deduce the type and keep the code to write and read short. Now the code is
more readable in terms of what it does, but even harder to know what the type is.
Most likely, the deduced type is not a floating-point type, but only with a knowledge
of the STL, can you say that.
Listing 1.20
1 auto v = std::vector<int>{3, 4, 5};
2 const auto size = v.size(); A Let the compiler deduce the type
This is where Concepts can help us use the best of the two worlds. Think about
what we usually need to know in these places. It is not necessarily the precise type
but the interface that type gives us. We expect, and the following code probably re-
quires, the type to be some sort of integral type. Do you remember the abbreviated
function template syntax? There we could prefix auto with a concept to constrain
the parameter’s type. We can do the exact same thing for auto variables in C++20.
40
Listing 1.21
1
Constrained placeholder types allow us to limit the type and its properties without
the need to specify an exact type.
We can do more than just constraint placeholder variables with Concepts. This syn-
tax applies to return types as well. Annotating an auto return-type has the same
benefit as for auto variables, or instead of typename, a user can see or lookup the
interface definition.
Concepts are more than just a replacement for SFINAE and a nicer syntax for
enable_if. While these two elements allowed us to write good generic code for
years, Concepts enlarge the application areas. We can use Concepts in more places
than enable_if.
One thing that gets pretty easy with Concepts is checking whether an object has a
certain function. In combination with constexpr if, one can conditionally call this
function if it exists. An example is a function template that sends data via the network.
Some objects may have a validation function to do a consistency check. Other objects,
probably simpler types, do not need such a function, and hence do not provide it.
Before Concepts, they would have probably provided a dummy implementation. In
terms of efficiency of run-time and binary size, this did not matter, thanks to state-
of-the-art optimizers. However, for us developers, it means to write and read this
nonsense function. We have maintained these functions over the years, just to do...
nothing. There are solutions using C++11, decltype in the trailing-return type, and
the comma operator to test a method’s existence. The thing is, writing this was a lot
Chapter 1: Concepts: Predicates for strongly typed generic code 41
of boilerplate code and needed a deeper understanding of all these elements and their
combination. With C++20, we can define a concept that has a requires-expression
containing a simple requirement, with the name SupportsValidation and we are
done.
1 template<typename T>
concept SupportsValidation = requires(T t)
Listing 1.22
2
3 {
4 t.validate();
5 };
This kind of if statement is evaluated at compile-time. Only one of the branches remains. The
other is discarded at compile-time, depending on the condition. The condition needs to be a
compile-time constant, for example, from a type-trait or a constexpr function.
1 template<typename T>
2 void Send(const T& data)
3 {
4 if constexpr(SupportsValidation<T>) { data.validate(); }
5
7 }
8
9 class ComplexType {
10 public:
11 void validate() const;
12 };
13
This allows us to provide types free of dummy functions and code. They are much
cleaner and portable this way.
When we create any wrapper, much like std::optional from C++17 ( Std-
Box 1.4 on page 45), that wrapper should behave as the object wrapped in the
std::optional. A wrapper<T> should behave like T. The standard states that
std:optional shall have a copy constructor if T is copy constructible, and that
it should have a trivial destructor if T is trivially destructible. This makes sense.
If T is not copyable, how can a wrapper like optional copy its contents? Even
if you find a way of doing it, the question is, why should such a wrapper behave
differently? Let’s take only the first requirement and try to implement this using
C++17, optional has a copy constructor if and only if T is copy constructible. This
task is fairly easy. There are even a type-traits std::is_copy_constructible and
std::is_default_destructible to do the job.
We create a class template with a single template parameter T for the type the op-
tional wraps. One way of storing the value is using placement new in an aligned
buffer. As this should not be a complete implementation of optional, let’s ignore
storing the value of T. An optional is default-constructible regardless of the prop-
erties of its wrapped type. Otherwise, an optional would not be optional, as it would
always need a value. For the constrained copy constructor, we need to apply an
enable_if and check whether T is copy constructible and whether the parameter
passed to the copy constructor is of type optional. This is an additional check we
have to do because of the templated version of this method. The resulting code is
shorter than the text needed to explain.
1 template<typename T>
2 class optional {
3 public:
Listing 1.24
4 optional() = default;
5
6 template<
7 typename U,
8 typename = std::enable_if_t<std::is_same_v<U, optional> and
Chapter 1: Concepts: Predicates for strongly typed generic code 43
9 std::is_copy_constructible_v<T>
10 >>
11 optional(const U&);
Listing 1.24
12
13 private:
14 storage_t<T> value;
15 };
After that, we can try out our shiny, admittingly reduced implementation. We
create a struct called NotCopyable. In that struct, we set the copy constructor as
well as the copy assignment operator as deleted. So far, we have looked only at the
copy constructor, but that is fine. The copy assignment operator behaves the same.
With NotCopyable, we can test our implementation. A quick test is to create the
object of optional<NotCopyable> and try to copy construct the second, passing
the first as the argument.
Listing 1.25
4 NotCopyable& operator=(const NotCopyable&) = delete;
5 };
6
7 optional<NotCopyable> a{};
8 optional<NotCopyable> b = a; B This should fail
That is great! The code compiles! Oh wait, that is not expected, is it? Did we make
a mistake? Yes, one which is, sadly, easy to make. The standard says specifically what
a copy constructor is and how it looks. A copy constructor is never a template. It
follows exactly the syntax T(const T&), that’s it. The question is now, what did we
do, or more specifically, what did we create? We created a conversion constructor.
Looking at the code from a different angle, the intended copy constructor takes a
U. The compiler cannot know that instantiation of this constructor fails for every
type except optional<T>. The correct way to implement this in C++17, and before,
was to derive optional from either a class with a deleted copy constructor and copy
assignment operator or derived from one with both defaulted. We can use std::
44
conditional to achieve this. That way, the copy operations of optional are deleted
by the compiler if a base class has them deleted. Otherwise, they are defaulted.
3 struct notCopyable {
4 notCopyable(const notCopyable&) = delete;
5 notCopyable operator=(const notCopyable&) = delete;
6 };
Listing 1.26
7
8 template<typename T>
9 class optional
10 : public std::conditional_t<std::is_copy_constructible_v<T>,
11 copyable,
12 notCopyable> {
13 public:
14 optional() = default;
15 };
Teach that to some person who is new to C++. We again have a lot of code for a sim-
ple task. How does this look in C++20? Much better. This is one case where the trail-
ing requires-clause shows its powers. In C++20, we can just write a copy constructor,
as we always do. No template is required. The class itself is already a template. But
we can apply the trailing requires to even non-templated methods. This helps us
because a trailing requires-clause doesn’t make the copy constructor anything else.
This method stays a copy constructor. It is even better. We can directly put our re-
quirement, in the form of the type-trait std::is_copy_constructible_v<T>, in
the trailing requires-clause. Absolutely beautiful and so much more readable than
any other previous approach. As another plus, this requires zero additional code,
which often looks unrelated, can be used by colleagues, and needs maintenance.
1 template<typename T>
class optional {
Listing 1.27
3 public:
4 optional() = default;
5
Other documents randomly have
different content
remain freely available for generations to come. In 2001, the Project
Gutenberg Literary Archive Foundation was created to provide a
secure and permanent future for Project Gutenberg™ and future
generations. To learn more about the Project Gutenberg Literary
Archive Foundation and how your efforts and donations can help,
see Sections 3 and 4 and the Foundation information page at
www.gutenberg.org.
Please check the Project Gutenberg web pages for current donation
methods and addresses. Donations are accepted in a number of
other ways including checks, online payments and credit card
donations. To donate, please visit: www.gutenberg.org/donate.
Most people start at our website which has the main PG search
facility: www.gutenberg.org.