Coroutines (C++20)

An ordinary function runs once, top to bottom, and returns. A coroutine can pause in the middle, hand a value back, and later resume exactly where it left off — with all its locals intact. That single ability powers lazy generators and clean asynchronous code without hand-written state machines. By the end of this lesson you'll know what co_await , co_yield , and co_return do, the role of the promise type and awaitables, and how to write a simple generator.

Learn Coroutines (C++20) in our free C++ course — an interactive lesson with worked examples, a practice exercise and a quick reference.

Part of the free C++ course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

Think of a coroutine as a bookmark in a novel . A normal function reads the whole book in one sitting and closes it. A coroutine reads a few pages, slips a bookmark in ( co_yield / co_await suspends), and sets the book down — remembering the exact page and everything that happened. Later, you pick it up and continue from the bookmark, not the beginning. co_return is finishing the book and putting it away for good. The "bookmark" is the coroutine frame, which stores your place and your local variables.

1. What Makes a Function a Coroutine

There is no special keyword on the declaration. A function becomes a coroutine the moment its body uses one of the three coroutine operators: co_await (suspend until something is ready), co_yield (produce a value and suspend), or co_return (finish, optionally with a value). The compiler then rewrites the function into a state machine and allocates a coroutine frame to hold its locals and resume point.

2. A Generator with co_yield

The classic use of coroutines is a generator : a function that produces a sequence one element at a time, lazily. Each co_yield hands a value to the caller and suspends; calling resume() continues from there. The generator type's promise_type is the compiler-required glue that says how to suspend and where each yielded value is stored.

Your turn. The coroutine below should emit even numbers. Replace the blank with the operator that produces a value and suspends:

3. Finishing with co_return

Where co_yield suspends to be resumed, co_return completes the coroutine — optionally with a final value, captured by the promise's return_value . After co_return the coroutine is done and cannot be resumed. This is the shape of a coroutine that computes one asynchronous or deferred result rather than a stream.

These lines form a tiny generator coroutine and drive it. Put them in the right order:

Define the coroutine (A), yield 1 then 2 in order (B, C), close it (D), create the generator (E), then resume it repeatedly to print each value (F).

4. co_await and Awaitables

The third operator, co_await , suspends a coroutine until some operation is ready — the foundation of asynchronous code. It works on an awaitable : a type exposing await_ready (is the result already available?), await_suspend (what to do while suspended, e.g. schedule a callback), and await_resume (the value the co_await expression yields on resumption). Awaiting an I/O operation lets the thread do other work instead of blocking.

When you call a coroutine, the compiler allocates a coroutine frame (often on the heap) holding its parameters, locals, and the point at which to resume. A std::coroutine_handle is a lightweight handle you use to resume() it or check if it is done() .

Every coroutine also has a promise type , found through its return type's promise_type . The compiler calls its members at key moments:

You rarely write this by hand in production: you reach for a ready-made type. C++23 adds std::generator ; before it, the cppcoro library was the common source of generators and tasks.

1. Which three operators can make a function a coroutine?

co_await, co_yield, and co_return — any one of them is enough.

Produces the value x to the caller and suspends the coroutine, to be resumed later from that point.

3. After co_return , can the coroutine be resumed?

No — co_return completes the coroutine; it is done and cannot resume.

Reuse the provided Gen type and write a coroutine that yields the first n Fibonacci numbers with co_yield . Drive it from main and print the sequence.

Practice quiz

What makes a C++ function a coroutine?

  • Using at least one of co_await, co_yield, or co_return in its body
  • Returning a special Task type
  • Declaring it noexcept
  • Marking it with the coroutine keyword

Answer: Using at least one of co_await, co_yield, or co_return in its body. A function is a coroutine if its body contains co_await, co_yield, or co_return; there is no separate function keyword.

What standard introduced coroutines as a language feature?

  • C++14
  • C++17
  • C++20
  • C++11

Answer: C++20. Coroutine support (co_await, co_yield, co_return) was standardized in C++20.

What does co_yield do in a coroutine?

  • Blocks until a lock is acquired
  • Returns a value and suspends, to be resumed later
  • Ends the coroutine permanently
  • Spawns a new thread

Answer: Returns a value and suspends, to be resumed later. co_yield produces a value to the caller and suspends the coroutine, which can then be resumed where it left off.

What does co_return do?

  • Awaits an awaitable
  • Suspends without a value
  • Yields the next element
  • Completes the coroutine, optionally returning a final value

Answer: Completes the coroutine, optionally returning a final value. co_return finishes the coroutine, optionally setting its final result, after which it cannot be resumed.

What is the promise type of a coroutine?

  • A compiler-required type that customizes the coroutine's behavior and result
  • The return value of co_await
  • A thread-safety guarantee
  • A std::promise used with futures

Answer: A compiler-required type that customizes the coroutine's behavior and result. Each coroutine has an associated promise_type (found via the return type) that controls suspension, the result, and lifecycle.

What does co_await operate on?

  • A raw pointer
  • An awaitable (a type providing await_ready / await_suspend / await_resume)
  • Any integer
  • Only std::future

Answer: An awaitable (a type providing await_ready / await_suspend / await_resume). co_await drives an awaitable through await_ready, await_suspend, and await_resume to suspend and resume the coroutine.

What is a generator in the coroutine sense?

  • A memory allocator
  • A random number engine
  • A thread pool
  • A coroutine that produces a sequence of values lazily via co_yield

Answer: A coroutine that produces a sequence of values lazily via co_yield. A generator yields values one at a time with co_yield, computing each only when the consumer asks for it.

When a coroutine suspends, what happens to its local variables?

  • They are copied to the caller
  • They become global
  • They are preserved in the coroutine frame so it can resume where it left off
  • They are lost

Answer: They are preserved in the coroutine frame so it can resume where it left off. The coroutine frame stores the locals and resume point, so execution continues correctly when resumed.

Which standard library facility for coroutine generators arrived in C++23?

  • std::async
  • std::generator
  • std::coroutine
  • std::lazy

Answer: std::generator. C++23 added std::generator; before that, people used libraries like cppcoro for ready-made coroutine types.

Why do coroutines enable lazy and async code so cleanly?

  • They can suspend and resume, so work is produced on demand or paused awaiting I/O
  • They disable the optimizer
  • They run faster than loops
  • They remove the need for functions

Answer: They can suspend and resume, so work is produced on demand or paused awaiting I/O. Because a coroutine can pause and continue, it naturally models lazy sequences and asynchronous waits without manual state machines.