Chapter 10: C++20

C++20 seems to be an exciting update. For example, as early as C++11, the Concept, which was eager to call for high-altitude but ultimately lost, is now on the line. The C++ Organizing Committee decided to vote to finalize C++20 with many proposals, such as Concepts/Module/Coroutine/Ranges/ and so on. In this chapter, we'll take a look at some of the important features that C++20 will introduce.

Concept

The concept is a further enhancement to C++ template programming. In simple terms, the concept is a compile-time feature. It allows the compiler to evaluate template parameters at compile-time, greatly enhancing our experience with template programming in C++. When programming with templates, we often encounter a variety of heinous errors. This is because we have so far been unable to check and limit template parameters. For example, the following two lines of code can cause a lot of almost unreadable compilation errors:

#include <list>
#include <algorithm>
int main() {
std::list<int> l = {1, 2, 3};
std::sort(l.begin(), l.end());
return 0;
}

The root cause of this code error is that std::sort must provide a random iterator for the sorting container, otherwise it will not be used, and we know that std::list does not support random access. In the conceptual language, the iterator in std::list does not satisfy the constraint of the concept of random iterators in std::sort. After introducing the concept, we can constrain the template parameters like this:

template <typename T>
requires Sortable<T> // Sortable is a concept
void sort(T& c);

abbreviate as:

template<Sortable T> // T is a Sortable typename
void sort(T& c)

Even use it directly as a type:

void sort(Sortable& c); // c is a Sortable type object

Let's look at a practical example. The following uses a requires expression to define a concept Addable, requiring that a type support + with a result convertible back to the type, and uses it to constrain a function template:

#include <concepts>
#include <iostream>

// Concept: T must support a + b, with a result convertible to T
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
};

template <Addable T>
T sum(T a, T b) { return a + b; }

int main() {
std::cout << sum(1, 2) << std::endl; // 3
std::cout << sum(1.5, 2.5) << std::endl; // 4
// sum("a", "b"); // compile error: does not satisfy Addable, with a clear message
}

When an argument of an unsatisfying type is passed, the compiler tells us directly that the constraint is not satisfied, instead of emitting a long cascade of internal template-instantiation errors.

Modules

Modules aim to solve the many problems of the traditional header mechanism: repeated parsing, macro pollution, include-order sensitivity, and slow compilation. A module is declared with export module and explicitly exports the entities visible to the outside:

// math.cppm — module interface unit
export module math;

export int add(int a, int b) {
return a + b;
}

Consumers use import instead of #include:

// main.cpp
import math;
import <iostream>;

int main() {
std::cout << add(1, 2) << std::endl;
}

Unlike the other examples in this book, compiling modules requires dedicated toolchain support and usually two steps (compile the module interface unit first, then the consumer); it cannot be built with a single clang++ file.cpp command. Consult your compiler's documentation for the exact build procedure.

Ranges

Ranges provide a higher-level, composable abstraction over the standard-library algorithms and iterators. With range adaptors and the pipe operator |, several lazy transformations can be chained in a declarative style:

#include <iostream>
#include <vector>
#include <ranges>

int main() {
std::vector<int> v{1, 2, 3, 4, 5, 6};
auto result = v | std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; });
for (int x : result) std::cout << x << ' '; // 4 16 36
std::cout << std::endl;
}

These views are lazily evaluated: the filtering and transformation are computed element by element only when result is iterated, with no intermediate container created.

Coroutines

A coroutine is a function that can be suspended and resumed. Any function whose body uses co_await, co_yield, or co_return is a coroutine. Note that C++20 provides only the language machinery plus the low-level support facilities in <coroutine>, leaving the "glue" such as the promise_type to the user or a library (a ready-made std::generator only arrived in C++23).

Here is a minimal lazy generator that yields values one at a time with co_yield:

#include <coroutine>
#include <iostream>
#include <optional>

template <typename T>
struct Generator {
struct promise_type {
T current;
Generator get_return_object() { return Generator{handle::from_promise(*this)}; }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T value) { current = value; return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
using handle = std::coroutine_handle<promise_type>;
handle h;
explicit Generator(handle h) : h(h) {}
~Generator() { if (h) h.destroy(); }
Generator(const Generator&) = delete;
Generator(Generator&& o) noexcept : h(o.h) { o.h = {}; }
std::optional<T> next() {
if (!h || h.done()) return std::nullopt;
h.resume();
if (h.done()) return std::nullopt;
return h.promise().current;
}
};

Generator<int> range(int a, int b) {
for (int i = a; i < b; ++i) co_yield i;
}

int main() {
auto g = range(1, 5);
while (auto v = g.next()) std::cout << *v << ' '; // 1 2 3 4
std::cout << std::endl;
}

A note on Contracts and Transactional Memory

A common misconception is worth clarifying: Contracts and Transactional Memory are not part of C++20.

This chapter therefore no longer presents them as C++20 features.

Conclusion

In general, I finally saw the exciting features of Concepts/Ranges/Modules in C++20. This is still full of charm for a programming language that is already in its thirties.

Further Readings