Apparatus

C++20 is awesome

Cat looking at The C++ Programming Language, 4th edition by Bjarne Stroustrup

The C++20 standard was recently completed, and is now only waiting for the final approval to become an ISO standard. In this post I'll discuss some of the most promising new features. A short disclaimer is in order. C++20 is brand new and compiler support is still experimental. I haven't really used it outside of toy programs. Still, these all are features I've been missing when writing C++ programs, and imagine myself using over and over again.

Better templates with concepts

This is the most obvious Long Awaited Big Thing that has finally been released as part of C++20. Constraints and consepts are the new way to formalize requirements that template parameters must satisfy. Such named sets of requirements (for example RandomAccessIterator, CopyConstructible and EqualityComparable) have of course existed in C++ terminology since the inception of STL. With the new version these requirements can be expressed as C++ code and enforced by the compiler in a more sane way.

C++ standard library already sees fetures implemented with concepts, most prominently with the newly furbished ranges and algorithms. Iterators and algorithms working with those iterators have always been at the core of the STL library. Iterator based algorithms have been a great way to decouple containers and algorithms operating on the containers. The syntax is admittedly verbose and cumbersome, and the compiler errors typically hard to interpret. This is especially true for situations where multiple algorithms have to be applied back-to-back. Think of the following code.

auto getTitlesOfPostsAboutKittens()
{
    auto posts = getAllPosts();
    // Remove posts that are not about kittens
    const auto new_end = std::remove_if(
        posts.begin(), posts.end(),
        [](const auto& post) { return post.category != "kittens"; });
    posts.erase(new_end, posts.end());
    // Get the titles
    std::vector<std::string> titles;
    std::transform(
        posts.begin(), posts.end(), std::back_inserter(titles),
        [](const auto& post) { return post.title; });
    return titles;
}

Separate statements are needed for filtering and transforming. Notoriously removing elements involves two function calls because the generic algorithm working on iterators doesn't know how to reclaim the space of the underlying container.

The new ranges library allows writing this more elegantly and without losing the generality and flexibility of the old approach.

auto getTitlesOfPostsAboutKittens()
{
    using namespace std::ranges;
    const auto posts = getAllPosts();
    auto posts_about_kittens = posts |
        views::filter([](const auto& post) { return post.category == "kittens"; }) |
        views::transform([](const auto& post) { return post.title; });
    return std::vector(posts_about_kittens.begin(), posts_about_kittens.end());
}

Note that filter and transform create views over the underlying container. That is why the return statement needs to construct a vector over the view before the posts variable is destructed. If the view can be used directly, the code is even shorter and more effective.

If there were some errors in the code the compiler would spit sensible error messages thanks to the requirements of the algorithms being expressed as concepts. For example, if the container used with sort didn't support random access, the error would be about concept violation, not about missing particular method (say operator[]).

More compile time computation

I love compile time computations, as is evident from my earlier post. C++20 has a prominent list of new features supporting constant expressions.

Constexpr iterators and algorithms. Yes, it has been possible to construct and manipulate (static) containers at compile time. With C++20 it's possible to manipulate those arrays using the same standard algorithms that are used to manipulate them at runtime. More specifically, C++20 introduces ConstexprIterator and marks algorithms accepting iterators constexpr. No more writing yet another insertion sort or a loop finding the maximum element of a constant evaluated array!

Less restrictions on expressions that may appear in core constant expressions. Virtual functions can now be marked constexpr and called at compile time when the type of the object is known. Allocating transitional memory for compile time computations — that is, allocating and then releasing it before the computation ends.

Forced constant evaluation. So far in many situations the compiler is free to chooce if a particular computation is done at compile time or at runtime. And a code path that the programmer may have intended to be used in compile time computations may accidentally appear in a context that's executed at runtime. C++20 introduces two new keywords to address this. constinit is used to define objects that are guaranteed to be initialized statically. consteval is used to define functions that are guaranteed to be evaluated at compile time (or it's an error). Also if there is a need to use simple implementation of an algorithm for compile time and an optimized one for runtime, it's possible to inspect within a function if it's being evaluated in constexpr context.

Language level support for coroutines

Event based programming has gained much popularity in the last few years. Major programming languages are providing core language and standard library level support for asynchronous programming. Python's asyncio and the whole design philosophy of Node.js are some of the most familiar examples. C++ has libraries that support asynchronous programmming, most notably ASIO, but more on that later.

I still remember the rush of enlightement when being introduced to coroutines as the tool for writing asynchronous code. Short recap: a coroutine is much like a function call, except that it can suspend itself and be resumed later. Endless amount of powerful techniques can be implemented using this rather abstract idea. A typical use case is writing asynchronous code in synchronous fashion.

Imagine writing a web server. The server gets a request to serve an user their profile page. The handler receiving the request will likely need to authenticate the user and fetch the profile data from a database. If both subtasks involve blocking IO calls, the CPU executing the handler would stay idle while it could be serving other clients. But if written as coroutine, the handler may suspend itself if the input is not readily available. The event loop dispatching the calls can then run hundreds or thousands of handlers in parallel without spawning a thread for each one.

With the new C++ coroutine syntax, the handler might look something like this:

task<Response> handleProfilePage(Request req)
{
    if (co_await checkUserCredentials(req.user)) {
        const auto profile = co_await geProfile(req.user);
        co_return renderProfilePage(profile);
    } else {
        co_return ResponseUnauthorized {};
    }
}

All right, I invented more than a few details here. Very little standard library support for coroutines made it to C++20. Thanks to the very general nature of the coroutine syntax, using the bare functionality is rather involved. So for now using coroutines requires 3rd party libraries to implement things like the promise object template task<>, and integration to the event loop orchestrating suspending and resuming the coroutines. But no doubt, with the new syntax being introduced as the default way to do coroutines, library support will follow. Adding more standard library support is a priority for C++23.

C++20 has native support for stackless coroutines only. In practice this means that a C++20 coroutine can only suspend itself in the base frame, limiting their use in callback-based code such as... well... code using the STL algorithms. It also means that 3rd party libraries implementing stackful coroutines (for example Boost, as always) will still have their uses in the foreseeable future.

Asynchronous libraries have long existed in the C++ ecosystem. The learning curve for these libraries are often steep even for the typical C++ level of steepness. Although part of the steep learning curve is due to the inherent complexity of asynchronous programming, solid language support will make a difference. While the support is far from complete in C++20, introducing core language level coroutines is a step towards more accessible asynchronous facilities.

Less boilerplate when writing comparison operators

C++20 introduces the three-way comparison operator <=>. It can be used to write one function to replace all six traditional comparisons (==, !=, <, <=, >=, >) in one go.

#include <compare>

struct IntegerPair {
    int x;
    int y;
};

constexpr auto operator<=>(const IntegerPair& p1, const IntegerPair& p2)
{
    const auto cmp = p1.x <=> p2.x;
    return cmp == 0 ? p1.y <=> p2.y : cmp;
}

static_assert(IntegerPair {0, 0} == IntegerPair {0, 0});
static_assert(IntegerPair {1, 3} < IntegerPair {2, 2});

Three-way comparison supports multiple set theoretic ordering notion (equality comparable, strong and weak ordering). And for the most obvious case where comparing a composite object is done by comparing its members, the three-way comparison can be declared as defaulted.

#include <compare>

struct IntegerPair {
    int x;
    int y;
    constexpr auto operator<=>(const IntegerPair&) const = default;
};

static_assert(IntegerPair {0, 0} == IntegerPair {0, 0});
static_assert(IntegerPair {1, 3} < IntegerPair {2, 2});

Yay!

And much more...

C++ uses release train model in shipping new features. Release dates are fixed, and features that are ready by the deadline are the ones shipped with the new standard. C++20 is a major release because many features that barely didn't make it to C++17 were now mature enough to go to C++20. That's why this time C++20 introduced lots and lots of features not included in this short overview.

A consolidated list of new features is available in Wikipedia.