Coroutines (C++20)

Reviewed & published by Brayan K

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.

Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

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.

#include <iostream>
#include <coroutine>
using namespace std;

// A minimal generator type. Its promise_type tells the compiler how the
// coroutine suspends and where each co_yield-ed value is stored.
struct Gen {
    struct promise_type {
        int current;
        Gen get_return_object() { return Gen{this}; }
        suspend_always initial_suspend() { return {}; }
        suspend_always final_suspend() noexcept { return {}; }
        suspend_always yield_value(int v) { current = v; return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };
    promise_type* p;
    explicit Gen(promise_type* pp) : p(pp) {}

    // Resume the coroutine and report whether it produced another value.
    bool next() {
        auto h = coroutine_handle<promise_type>::from_promise(*p);
        h.resume();
        return !h.done();
    }
    int value() const { return p->current; }
};

// This IS a coroutine, because it uses co_yield.
Gen countTo(int n) {
    for (int i = 1; i <= n; ++i)
        co_yield i;          // produce i, then SUSPEND until resumed
}

int main() {
    Gen g = countTo(3);
    while (g.next()) cout << g.value() << ' ';   // 1 2 3
    cout << endl;
    return 0;
}

// ✅ Expected output:
//    1 2 3

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

#include <iostream>
#include <coroutine>
using namespace std;

struct Gen {
    struct promise_type {
        int current;
        Gen get_return_object() { return Gen{this}; }
        suspend_always initial_suspend() { return {}; }
        suspend_always final_suspend() noexcept { return {}; }
        suspend_always yield_value(int v) { current = v; return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };
    promise_type* p;
    explicit Gen(promise_type* pp) : p(pp) {}
    bool next() {
        auto h = coroutine_handle<promise_type>::from_promise(*p);
        h.resume();
        return !h.done();
    }
    int value() const { return p->current; }
};

Gen evens(int count) {
    int n = 0;
    for (int i = 0; i < count; ++i) {
        // 🎯 YOUR TURN — produce the value 'n' and suspend, then advance.
        ___ n;          // 👉 use the keyword:  co_yield
        n += 2;
    }
}

int main() {
    Gen g = evens(4);
    while (g.next()) cout << g.value() << ' ';
    cout << endl;
    // ✅ Expected output:  0 2 4 6
    return 0;
}

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.

#include <iostream>
#include <coroutine>
using namespace std;

// A tiny eager "task" that holds a single result produced by co_return.
struct Task {
    struct promise_type {
        int result = 0;
        Task get_return_object() { return Task{this}; }
        suspend_never initial_suspend() { return {}; }   // run immediately
        suspend_always final_suspend() noexcept { return {}; }
        void return_value(int v) { result = v; }         // captures co_return
        void unhandled_exception() {}
    };
    promise_type* p;
    explicit Task(promise_type* pp) : p(pp) {}
    int get() const { return p->result; }
};

// A coroutine because it uses co_return.
Task square(int x) {
    co_return x * x;         // finish the coroutine with a final value
}

int main() {
    Task t = square(6);
    cout << "result = " << t.get() << endl;   // result = 36
    return 0;
}

// ✅ Expected output:
//    result = 36

🧩 Reorder Challenge

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

A. Gen oneTwo() {
B.     co_yield 1;
C.     co_yield 2;
D. }
E. Gen g = oneTwo();
F. while (g.next()) std::cout << g.value() << ' ';

Correct order: A → B → C → D → E → F

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.

🔎 Deep Dive: the coroutine frame and promise type

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.

Pro Tips

🧠 Quick Recall

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

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

2. What does co_yield x; do?

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.

Common Errors (and the fix)

📋 Quick Reference

Keyword / TypeMeaning
co_yield v;Produce a value and suspend
co_return v;Finish the coroutine with a result
co_await a;Suspend until awaitable a is ready
promise_typeCustomizes suspend / yield / return
coroutine_handleResume / query / destroy the frame
std::generatorC++23 ready-made generator type
#include <coroutine>Required header

Mini-Challenge: a Fibonacci generator

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.

#include <iostream>
#include <coroutine>
using namespace std;

// 🎯 MINI-CHALLENGE: a Fibonacci generator
// Reuse the Gen type below. Write a coroutine  fib(int count)  that
// uses co_yield to produce the first 'count' Fibonacci numbers
// (starting 0, 1, 1, 2, 3, ...). Then print them in main().
//
// ✅ Expected output for fib(7):
//    0 1 1 2 3 5 8

struct Gen {
    struct promise_type {
        long current;
        Gen get_return_object() { return Gen{this}; }
        suspend_always initial_suspend() { return {}; }
        suspend_always final_suspend() noexcept { return {}; }
        suspend_always yield_value(long v) { current = v; return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };
    promise_type* p;
    explicit Gen(promise_type* pp) : p(pp) {}
    bool next() {
        auto h = coroutine_handle<promise_type>::from_promise(*p);
        h.resume();
        return !h.done();
    }
    long value() const { return p->current; }
};

// Gen fib(int count) {
//     your code here
// }

int main() {
    // your code here
    return 0;
}

🎉 Lesson Complete

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.

Continue this course

Frequently asked questions

What exactly makes a function a coroutine?

There is no special keyword on the declaration. A function becomes a coroutine simply by using co_await, co_yield, or co_return anywhere in its body. The compiler then transforms it into a state machine with an associated coroutine frame and promise type.

What is the promise type and do I have to write it?

Every coroutine has an associated promise_type, located through its return type, that customizes how it starts, suspends, yields, and finishes. The standard library and third-party libraries provide ready-made return types (each carrying a promise_type) so that, in practice, you usually use an existing generator or task type rather than writing the promise machinery yourself.

What is the difference between co_yield and co_return?

co_yield produces a value and suspends the coroutine so it can be resumed later from the same point. co_return finishes the coroutine, optionally providing a final value, after which it cannot be resumed. A generator typically uses co_yield in a loop and may end with an implicit or explicit co_return.

What is an awaitable?

An awaitable is any type you can pass to co_await. It exposes await_ready, await_suspend, and await_resume, which the compiler calls to decide whether to suspend, what to do while suspended, and what value the co_await expression produces on resumption. Awaitables are how coroutines integrate with asynchronous operations.

What library support exists for coroutines?

C++20 standardized the language machinery but shipped few ready-to-use types. Many projects used the cppcoro library for generators and tasks. C++23 added std::generator, a standard coroutine-based generator type, making lazy sequences far easier to write without third-party code.

Related lessons