Coroutines

A coroutine is a function that can suspend in the middle and resume later. The compiler turns its body into a state machine on the heap, and the state is reachable through a std::coroutine_handle.

A coroutine is not a thread and not a callback: it runs on whatever thread resumes it. That is what makes it the tool for a generator, for a task that waits on I/O, and for the co_yield of lesson 116. This lesson builds the smallest coroutine type that can be resumed by hand.

The compiler reports the frame allocation as a call to operator new, and a promise type may define its own to place the frame in a pool. A coroutine that is created often and completes at once is the case where that allocation is worth measuring.

The promise type is also where a coroutine reports an exception: unhandled_exception stores it and the caller rethrows it, the same transfer that the future of lesson 110 performs across a thread.

A coroutine resumed by hand

A function is a coroutine when its body contains co_await, co_yield or co_return. Its return type must contain a promise_type, and the compiler uses that type to build the frame and to decide what happens at the start and at the end.

The program's coroutine prints one line, suspends, prints the next, and suspends again. Main resumes it three times and finds it finished, so the output is created, one, two, three, finished.

initial_suspend returns suspend_always, so nothing runs until the first resume. That is the lazy choice, and it is what makes a coroutine safe to create before the caller is ready.

#include <coroutine> #include <exception> #include <iostream> struct task { struct promise_type { task get_return_object() { return task{std::coroutine_handle<promise_type>::from_promise(*this)}; } std::suspend_always initial_suspend() noexcept { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() { std::terminate(); } }; explicit task(std::coroutine_handle<promise_type> handle) : handle_(handle) {} ~task() { if (handle_) { handle_.destroy(); } } void step() { if (!handle_.done()) { handle_.resume(); } } std::coroutine_handle<promise_type> handle_; }; task steps() { std::cout << "one\n"; co_await std::suspend_always{}; std::cout << "two\n"; co_await std::suspend_always{}; std::cout << "three\n"; } int main() { task body = steps(); std::cout << "created\n"; body.step(); body.step(); body.step(); std::cout << (body.handle_.done() ? "finished" : "suspended") << '\n'; }

g++ -std=c++23 -Wall -Wextra manual.cpp -o manual
./manual

created
one
two
three
finished

co_return delivers a value

co_return value calls return_value on the promise, which is where the result is stored. After the final suspend the caller reads it from handle.promise().value.

The program's coroutine returns 42 at once, and get resumes it once and reads the promise. The value is there because the coroutine ran to its end in that one resume.

A coroutine that suspends before the return needs more resumes, which is why the accessor checks done() or resumes in a loop. The generator of the next lesson hides that loop behind an iterator.

#include <coroutine> #include <exception> #include <iostream> struct value_task { struct promise_type { int value = 0; value_task get_return_object() { return value_task{std::coroutine_handle<promise_type>::from_promise(*this)}; } std::suspend_always initial_suspend() noexcept { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void return_value(int result) { value = result; } void unhandled_exception() { std::terminate(); } }; explicit value_task(std::coroutine_handle<promise_type> handle) : handle_(handle) {} ~value_task() { if (handle_) { handle_.destroy(); } } int get() { handle_.resume(); return handle_.promise().value; } std::coroutine_handle<promise_type> handle_; }; value_task answer() { co_return 42; } int main() { value_task task = answer(); std::cout << task.get() << '\n'; }

g++ -std=c++23 -Wall -Wextra return.cpp -o return_value
./return_value

42

An awaiter with two sides

The object after co_await is an awaiter, and the compiler calls three methods on it: await_ready to decide whether to suspend, await_suspend after the suspension, and await_resume when the coroutine continues.

The program's awaiter prints in await_suspend and in await_resume, so the output shows which side of the pause each line came from: before, suspend, resume, after.

The handle passed to await_suspend is the one to store if the awaiter wants to resume the coroutine later. Resuming from another thread is legal and is the foundation of every task type, as long as the frame is not resumed twice at once.

#include <coroutine> #include <exception> #include <iostream> struct task { struct promise_type { task get_return_object() { return task{std::coroutine_handle<promise_type>::from_promise(*this)}; } std::suspend_always initial_suspend() noexcept { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() { std::terminate(); } }; explicit task(std::coroutine_handle<promise_type> handle) : handle_(handle) {} ~task() { if (handle_) { handle_.destroy(); } } void step() { if (!handle_.done()) { handle_.resume(); } } std::coroutine_handle<promise_type> handle_; }; struct logger { bool await_ready() const noexcept { return false; } void await_suspend(std::coroutine_handle<>) const noexcept { std::cout << "suspend "; } void await_resume() const noexcept { std::cout << "resume "; } }; task sequence() { std::cout << "before "; co_await logger{}; std::cout << "after\n"; } int main() { task body = sequence(); body.step(); body.step(); }

g++ -std=c++23 -Wall -Wextra awaiter.cpp -o awaiter
./awaiter

before suspend resume after

A return type without a promise

The compiler needs a promise type to build the frame of a coroutine. A function with co_await and an ordinary return type has none, so the function does not compile and the message says the promise type cannot be found.

The program writes a coroutine that claims to return int. The error names the function and the missing promise, and it is reported at the co_await line rather than at the declaration.

The fix is a return type with a nested promise_type, as in every earlier section, or a standard one such as std::generator from C++23. Writing the type once and reusing it is the usual approach, which is what a library's task type is.

#include <coroutine> int broken() { co_await std::suspend_always{}; co_return 1; }

g++ -std=c++23 -Wall -Wextra broken.cpp -o broken

broken.cpp: In function 'int broken()':
broken.cpp:5:5: error: unable to find the promise type for this coroutine
5 | co_await std::suspend_always{};
| ^~~~~~~~

Exercises

  1. Add a counter to the first program that prints the number of resumes it has seen.
  2. Make the awaiter print in `await_ready` as well and check when the method is called.
  3. Change `initial_suspend` to `suspend_never` in the first program and observe what happens before the first step.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. When is a function a coroutine?

    Answer

    When its body contains co_await, co_yield or co_return

    One of those three expressions in the body makes the compiler build a frame for the function.

  2. What does the compiler require of the return type of a coroutine?

    Answer

    A nested promise_type

    The promise type is what the compiler instantiates to store the result and to control the suspends.

  3. What does initial_suspend returning suspend_always mean?

    Answer

    The body does not run until the first resume

    The initial suspend is the point before the body, and suspend_always leaves the coroutine paused there.

  4. What does this program print? #include <coroutine> #include <iostream> struct task { struct promise_type { task get_return_object() { return {std::coroutine_handle<promise_type>::from_promise(*this)}; } std::suspend_always initial_suspend() noexcept { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {} }; std::coroutine_handle<promise_type> h; void step() { if (!h.done()) h.resume(); } ~task() { if (h) h.destroy(); } }; task steps() { std::cout << "one "; co_await std::suspend_always{}; std::cout << "two "; co_await std::suspend_always{}; std::cout << "three "; } int main() { task t = steps(); std::cout << "created "; t.step(); t.step(); t.step(); std::cout << (t.h.done() ? "finished" : "suspended"); }

    Answer

    created one two three finished

    The body is lazy, so created comes first, and the third resume reaches the end.

  5. What does this program print? #include <coroutine> #include <iostream> struct value_task { struct promise_type { int value = 0; value_task get_return_object() { return {std::coroutine_handle<promise_type>::from_promise(*this)}; } std::suspend_always initial_suspend() noexcept { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void return_value(int v) { value = v; } void unhandled_exception() {} }; std::coroutine_handle<promise_type> h; ~value_task() { if (h) h.destroy(); } }; value_task answer() { co_return 42; } int main() { auto t = answer(); t.h.resume(); std::cout << t.h.promise().value; }

    Answer

    42

    return_value stores 42 in the promise, and the resume runs the coroutine to its end.

  6. What does this program print? #include <coroutine> #include <iostream> struct task { struct promise_type { task get_return_object() { return {std::coroutine_handle<promise_type>::from_promise(*this)}; } std::suspend_always initial_suspend() noexcept { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {} }; std::coroutine_handle<promise_type> h; void step() { if (!h.done()) h.resume(); } ~task() { if (h) h.destroy(); } }; struct logger { bool await_ready() const noexcept { return false; } void await_suspend(std::coroutine_handle<>) const noexcept { std::cout << "suspend "; } void await_resume() const noexcept { std::cout << "resume "; } }; task sequence() { std::cout << "before "; co_await logger{}; std::cout << "after"; } int main() { task t = sequence(); t.step(); t.step(); }

    Answer

    before suspend resume after

    await_suspend runs at the pause and await_resume runs when the coroutine continues.

  7. Which method decides whether the coroutine suspends at a co_await?

    Answer

    await_ready

    A true from await_ready skips the suspension, and then await_resume runs at once.

  8. Where does a coroutine keep its state between resumes?

    Answer

    In a frame allocated on the heap, reached through the coroutine handle

    The compiler allocates the frame, and the handle is the pointer to it.

  9. What happens when a coroutine's return type has no promise_type?

    Answer

    The function does not compile: the promise type cannot be found

    Without the promise type the compiler cannot build the frame, so the co_await is an error.

  10. Which method stores the value of co_return 42?

    Answer

    return_value on the promise

    The compiler calls return_value with the expression, and the promise holds it.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev