Skip to content

Last updated:2026-10-12

Getting Started with C++20 Coroutines

1. What Are Coroutines

Since C++20, C++ finally supports coroutines; C++23 is expected to bring additional support that at least covers the most common use cases.

A coroutine is a kind of program component that generalizes the subroutine of cooperative multitasking, allowing execution to be suspended and resumed. Compared with subroutines, coroutines are more general and more flexible, but in practice they are not used as widely as subroutines. Coroutines are a better fit for implementing familiar program components such as cooperative multitasking, exception handling, event loops, iterators, infinite lists, and pipes.

This passage from Wikipedia describes the basic idea of coroutines.

From the C++ perspective, you can simply understand a coroutine as any function that contains co_return, co_yield, or co_await.

C++20 coroutines are currently syntactic sugar over function objects: the compiler generates a code framework around the coroutine. This code relies on user-defined promise types. That may change after C++23, but until then we play by the C++20 rules and write these types ourselves.

co_return, co_yield, and co_await were mentioned above; they will be explained in later sections.

2. Step By Step Coroutine

Next, let's start with a simple coroutine example. The following coroutine does nothing at all — let's call it the HelloTask coroutine.

#include <coroutine>
struct HelloTask {
    struct promise_type {
        HelloTask get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };
};
HelloTask myCoroutine() {
    co_return; // make it a coroutine
}
int main() {
    HelloTask x = myCoroutine();
}

Compile and run this code:

g++ hello.cpp -fcoroutines -std=c++20

Your compiler needs to support C++20 features.

Let's walk through this HelloWorld program from top to bottom and see how it differs from everyday development.

2.1 Header

First, the header introduces coroutine. Here I went straight to the gcc-10 source code to see what it actually does.

https://github.com/gcc-mirror/gcc/blob/releases/gcc-10/libstdc++-v3/include/std/coroutine

Its contents include the following:

// 用于发现协程promise_type的特征类型
// 核心代码:using promise_type = typename _Result::promise_type;
1.coroutine_traits

// 协程句柄,用于管理暂停或执行的协程以及Promise
2.coroutine_handle
  // coroutine_handle全特化版本  是其他特化的公开基类。它保有handle的底层     地址。
  2.1 coroutine_handle<void>

  // coroutine_handle泛化版本
  2.2 template <typename _Promise>
      struct coroutine_handle : coroutine_handle<>


  // coroutine_handle全特化版本 处理无操作协程
  2.3 template <>
      struct coroutine_handle<noop_coroutine_promise> : public          coroutine_handle<>
3.平凡多可等待对象 Trivial awaitables
  // await表达式应该暂停
  3.1 suspend_always
  // await表达式不应该暂停
  3.2 suspend_never

You can see it contains quite a few specific terms:

  • coroutine_handle
  • Promise
  • awaitable

and so on.

The coroutine material in the rest of this article is built around these.

2.2 Promise

Continuing with the HelloTask above, you will find it contains this snippet:

struct HelloTask {
    struct promise_type {
        HelloTask get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };
};

So the question is: why do we need to write promise_type here?

The reason for writing promise_type is the handle mentioned in section 2.1 above: when the compiler creates the coroutine's handle, it needs to know the promise_type of that coroutine object, so basically every coroutine object has a HelloTask::promise_type structure. Careful readers will have already figured it out: the promise_type is passed over, parsed inside the Handle, and used to determine the behavior of the user-defined coroutine object.

Let's take a closer look at what goes inside promise_type. Its contents are fixed, and as the later material grows richer, more and more functions get involved. For example, return_void is used here; later, when co_return returns an object, a different function will be used instead.

get_return_object can return the current user-defined object. Here it does nothing; picking up from section 2.1 above, we can use a coroutine_handle instead.

struct promise_type {
    using Handle = std::coroutine_handle<promise_type>;
    HelloTask get_return_object() {
        return HelloTask{Handle::from_promise(*this)};
    }
  // other member function
}

You can see it uses the coroutine_handle's from_promise member to construct an object.

initial_suspend: tells the compiler whether the coroutine should stay suspended the first time it executes, i.e. whether it stays suspended when the coroutine is instantiated.

final_suspend: the opposite of initial_suspend. After the coroutine body completes, there is a final_suspend at the end of the coroutine. This gives you a chance to do additional cleanup.

Both of these return an awaitable; here the suspend_always from the header file is used — of course a user-defined awaitable works too, which will be covered in a later article.

The source code of suspend_always and suspend_never is as follows:

struct suspend_always
{
  constexpr bool await_ready() const noexcept { return false; }

  constexpr void await_suspend(coroutine_handle<>) const noexcept {}

  constexpr void await_resume() const noexcept {}
};

struct suspend_never
{
  constexpr bool await_ready() const noexcept { return true; }

  constexpr void await_suspend(coroutine_handle<>) const noexcept {}

  constexpr void await_resume() const noexcept {}
};

These contain several members:

  • await_ready
  • If you already know the result is ready, can it complete synchronously?
  • await_suspend
  • The return value can be bool, void, or a coroutine handle.
  • Here it returns void, which immediately returns control to the current coroutine's caller/resumer (this coroutine remains suspended); if await_suspend returns bool instead, true means control goes to the caller/resumer and false means the current coroutine is resumed. Returning a coroutine handle will be covered in a later article.
  • await_resume
  • Its result is the result of the whole co_await expression. So the return value does not have to be void — it can also be a custom object and so on.

return_void: when co_return; is used, return_void is called.

unhandled_exception: used to catch exceptions.

Finally, one step is still missing: creating the coroutine. According to the definition at the beginning, a coroutine is any function containing co_return, co_yield, or co_await. Hence:

HelloTask myCoroutine() {
    co_return; // make it a coroutine
}

2.3 The Overall Flow

What does the whole call flow look like? This section walks through the execution logic of the code above in detail. Before starting, let's modify the code and add some print statements:

#include <iostream>
#include <coroutine>
struct HelloTask {
    struct promise_type {
        HelloTask get_return_object() { std::cout << "get_return_object" << std::endl; return { }; }
        std::suspend_never initial_suspend() { std::cout << "initial_suspend" << std::endl; return {}; }
        std::suspend_never final_suspend() noexcept { std::cout << "final_suspend" << std::endl;return {}; }
        void return_void() { std::cout << "return_void" << std::endl; }
        void unhandled_exception() { std::cout << "unhandled_exception" << std::endl; }
    };
};
HelloTask myCoroutine() {
    std::cout << "create coroutine" << std::endl;
    co_return; // make it a coroutine
}
int main() {
    std::cout << "start main" << std::endl;
    HelloTask x = myCoroutine();
    std::cout << "end main" << std::endl;
}

The output is then:

start main
get_return_object
initial_suspend
create coroutine
return_void
final_suspend
end main

The flow is as follows:

  • start main
  • The coroutine object is allocated on the heap, the promise is constructed, and promise.get_return_object is called to return the current object (this is where get_return_object is printed); the point of doing this is to save the result in a local variable.
  • promise.initial_suspend, which prints initial_suspend; this triggers co_await, prints create coroutine, and after the coroutine body runs, co_return is called.
  • co_return calls promise.return_void, printing return_void.
  • Finally, promise.final_suspend prints final_suspend.
  • end main

There is a great deal more to coroutines; this section is the opening chapter, and more will keep coming~

Comments