Chapter 10: C++20

June 7, 2026 · View on GitHub

[TOC]

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.

  • Contracts were once in the C++20 working draft but were removed before the standard was published; they are now an important feature targeting C++26 (see this repository's C++26 tracking issue #318).
  • Transactional Memory exists only as a Technical Specification (TS) and was never merged into the C++20 standard.

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.

Table of Content | Previous Chapter | Next Chapter: Introduction of C++23

Further Readings

Licenses

Creative Commons License
This work was written by Ou Changkun and licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License. The code of this repository is open sourced under the MIT license.