The PracticalProgrammers Guide To C20
The PracticalProgrammers Guide To C20
The PracticalProgrammers Guide To C20
Programmer’s Guide to
C++20
Giuseppe D’Angelo and Ivan Čukić
C++20 provides C++ with even more power and expressiveness.
However, a list of the new C++20 features often sounds like a A concept
rules-lawyer’s minutiae. While the standard contains dozens of specifies the
improvements and fixes, we’re going to focus on the changes we
template author’s
think will make the biggest differences to the everyday C++ pro-
grammer. Of course, we’ll be talking in detail about the biggest expectations for
four improvements (concepts, coroutines, modules, and ranges), how the template
but we’ll also share a couple of smaller changes that we think are should be used.
interesting.
Concepts
Let’s see what this looks like in action by using the well-worn facto-
rial, the “hello world” of functions that every programmer is prob-
ably tired of seeing written out in code. We apologize for bringing
one more version of factorial into the world, but bear with us,
because it will turn out to be handy for explaining more about
concepts in a bit.
#include <concepts>
#include <iostream>
int main() {
std::cout << factorial(10) << std::endl;
return 0;
}
As you can see, this is not too different than a plain-old factorial
function, although we use std::integral instead of an integer type.
std::integral is a concept, and it tells the compiler that as long as
the parameter conforms to a standard integer type (like any of the
built-in 8-, 16-, 32-, or 64-bit signed or unsigned types), go ahead
and accept this invocation of the function template as valid. In
turn, since we use a concept-enforced std::integral auto as the
return type, it ensures our return type will also be an integer class.
Concepts turn our simple function into a generic one that handles
all integral types, which makes it more flexible and reusable.
The compiler
Concepts will also let us easily extend our function to other
integer or even non-integer types as we’ll soon see. knows exactly what
is expected and
Concepts and improved errors can produce much
One big benefit of using concepts manifests itself when we start more meaningful
using the template above. The compiler knows exactly what error diagnostics
is expected and can produce much more meaningful error
when constraints
diagnostics when constraints aren’t met. For example, here’s what
happens when you try to use the function template above with aren’t met.
something that’s not an integer.
main.cpp: In instantiation of ‘auto [requires ::Integral<<placeholder>, >]
factorial(auto:11) [with auto:11 = double]’:
main.cpp:50:33: required from here
main.cpp:27:30: error: use of function ‘auto [requires ::Integral<<placeholder>, >]
factorial(auto:11) [with auto:11 = double]’ with unsatisfied constraints
27 | else return a * factorial(a - 1);
| ~~~~~~~~~^~~~~~~
main.cpp:25:15: note: declared here
25 | Integral auto factorial(Integral auto a){
| ^~~~~~~~~
main.cpp:25:15: note: constraints not satisfied
main.cpp: In function ‘int main()’:
main.cpp:50:33: error: use of function ‘auto [requires ::Integral<<placeholder>, >]
factorial(auto:11) [with auto:11 = double]’ with unsatisfied constraints
50 | std::cout << factorial(-10.5) << std::endl;
| ^
main.cpp:25:15: note: declared here
25 | Integral auto factorial(Integral auto a){
| ^~~~~~~~~
main.cpp:25:15: note: constraints not satisfied
main.cpp: In instantiation of ‘auto [requires ::Integral<<placeholder>, >]
factorial(auto:11) [with auto:11 = double]’:
main.cpp:50:33: required from here
main.cpp:8:9: required for the satisfaction of ‘Integral<auto:11>’ [with auto:11 =
double]
main.cpp:9:26: note: the expression ‘std::is_integral<_Tp>::value [with _Tp = double]’
evaluated to ‘false’
9 | std::is_integral<T>::value;
| ^~~~~
#include <iostream>
#include <complex>
#include <concepts>
using namespace std::complex_literals;
template<typename T>
concept Continuous = Complex<T> || std::floating_point<T>;
Concept specialization
Multiple concepts for the same type are handled by the compiler
in a process called partial ordering of constraints. Fundamentally,
this means that if more than one constraint matches a function,
template, overload, or argument, the compiler preferentially
chooses one that is more specialized before one that is less
specialized. This is a formalized way to specify to the compiler the
behavior that a programmer might intuitively expect.
int main() {
f(3.14f); // Case A:
compiler calls f #1
f(0.707 – 0.707i); // Case B: Concepts bring the
compiler calls f #2
return 0; promise and power
}
out of the hands
Example 4: Concept specialization of library authors
and into the
The “tighter” the concept is defined, the higher it will be prioritized
hands of everyday
when the compiler has more than one competing option that
matches. So, any type matching the std::floating_point concept programmers.
(like 3.14f in case A, as well as any other float or double) will match
both definitions of function f. However, because definition #1 is
more specialized – technically speaking, f #1 is subsumed by f #2
– the compiler will use definition #1 for floating point numbers. In
case B, the only concept that applies to complex numbers is our
custom Continuous concept, so the compiler chooses function
definition #2.
Ranges
Next on the C++20 hit parade are ranges, which can be thought
of as “Unix pipes brought to C++”. Because they don’t require
loops and because range views are generally value-based (non-
mutating), there’s less room for error – no off-by-one errors
or memory allocation issues. Ranges bring a bit of the bliss of
functional programming to C++. Let’s see them in action.
KDAB — the Qt, OpenGL and C++ experts 6
Range views don’t have off-by-one errors or memory allocation
issues.
#include <ranges>
#include <iostream>
int main()
{
auto ints = std::ranges::iota_view{1, 33};
// Half-closed range,
32 is last
#include <iostream>
#include <vector>
#include <range/v3/view.hpp>
#include <range/v3/action.hpp>
#include <range/v3/view/istream.hpp>
#include <range/v3/range/conversion.hpp>
Realizing ranges
Unfortunately, there’s a catch, and that is, the code above won’t
run with out-of-the-box C++20 alone. The code here is using
Eric’s implementation of ranges that unfortunately, to prevent
delaying the standard, was only partially adopted by the standards
committee. While ranges in their fullest expression are extremely
powerful, the C++20 version is lacking some key capabilities like
group_by, zip, concatenate, enumerate, and lots more that you’d
need for many pretty common uses.
The good news is that you can experiment around with the ranges
that C++20 does provide today. If you get hooked and want a
bit more power, you can download working and tested range-v3
code while you’re waiting for more range expressiveness to make
its way into the standard. Hopefully by C++23, a much more
comprehensive set of range functionality should be able to clear
the standards committee.
Modules
Coroutines
The last big thing that C++20 brings to the party is coroutines.
Have you ever done any of the following?
task<> tcp_echo_server() {
char data[1024];
for (;;) {
size_t n = co_await socket.async_read_
some(buffer(data));
co_await async_write(socket, buffer(data, n));
}
}
If our echo server example were any more complex, you might be
able to justify using multi-threaded code. In this case though, the Coroutines can
cooperatively threaded model provided by coroutines does the
trick. It’s “nearly multi-threaded” without having to worry about make it easy to
spinning up threads, adding locks or synchronization primitives, introduce a level
or figuring out how to cleanly tear down the thread. And that’s of simple “parallel”
the case for an awful lot of event-driven things in our programs. execution without
Multithreading takes far more work to “do it right” to justify using worrying about
it in every case. Coroutines can make it easy to introduce a level
callbacks or state
of simple “parallel” execution without worrying about callbacks or
state machines. Just remember it’s not intended to replace real machines.
multi-threading and don’t get carried away.
Anything else?
We’ve talked about the biggest four changes in C++20, but there
are two other smaller yet still impactful changes that are worth
pointing out.
std::format
The C++ community has two options for creating formatted
strings: the archaic past of printf/sprintf strings that are both
cryptic and ultra-dangerous, or modern stream operators that Be jealous of
are terrible for localization. Thankfully, C++20 has adopted Victor
Python string
Zverovich’s fmt library in the form of std::format, which solves
both of these issues. This is because std::format gives us safe formatting no
string formatting that complements the existing C++ methods of more.
string output.
Example 12: Two ways to generate “hello world!” with positional args
What’s the big deal? The great part about spaceship is that it
makes writing your own operators much easier. It’s a bit of a pain
to supply all six comparison operators for your own classes (<,
<=, ==, >, >=, !=). That becomes especially true if you provide
comparisons from your class to other built-in or library classes
(and vice versa), when the number of comparison operators can
explode. Thankfully, now you just need to write one spaceship
operator for your class, and one spaceship for each comparison
type. That’s it – two trivial operators instead of 18 for the below
example.
KDAB — the Qt, OpenGL and C++ experts 14
Spaceship makes writing your own operators much easier.
#include <compare>
#include <iostream>
class ExValue {
public:
constexpr explicit ExValue(int val): value{val} { }
auto operator<=>(const ExValue& rhs) const = default;
constexpr auto operator<=>(const int& rhs) const {
return value - rhs;
}
private:
int value;
};
int main() {
std::cout << whatis(ExValue(10), ExValue(12)) << std::endl;
std::cout << whatis(ExValue(12), ExValue(10)) << std::endl;
std::cout << whatis(ExValue(8), ExValue(8)) << std::endl;
std::cout << whatis(10, ExValue(12)) << std::endl;
std::cout << whatis(100, ExValue(50)) << std::endl;
std::cout << whatis(ExValue(10), 40) << std::endl;
std::cout << whatis(ExValue(40), 10) << std::endl;
Also note that we didn’t even have to write our type’s spaceship
function – using default allows the compiler to synthesize one
in many simple cases. We get all 18 operators for “free” because
the compiler now converts all comparison operations into
spaceship automatically. When the compiler sees A < B, it silently
interprets that as (A <=> B) < 0, and similarly transforms any other
comparison operator.
Summary
The new standard has many changes that will mostly be of inter-
est to library builders and language experts, things like consteval,
constinit, no_unique_address, lambda function improvements,
and others. These all help to improve the foundation of the lan-
guage, make code more efficient or easier to write, close gaps in
the standard, or lay groundwork for upcoming changes in C++23.
But the six additions that we talk about in this whitepaper are the
ones we think will make the biggest difference in the lives of prac-
tical programmers.
www.kdab.com