Technical Background

Stackless

C++20 coroutines are stackless, meaning they don’t have their own stack.

A stack in C++ describes the callstack, i.e. all the function frames stacked. A function frame is the memory a function needs to operate, i.e. a slice of memory to store its variables and information such as the return address.

The size of a function frame is known at compile time, but not outside the compile unit containing its definition.
int bar() {return 0;} // the deepest point of the stack
int foo() {return bar();}

int main()
{
    return bar();
}

The call stack in the above example is:

main()
  foo()
    bar()
Diagram

Coroutines can be implemented a stackful, which means that it allocates a fixes chunk of memory and stacks function frames similar to a thread. C++20 coroutines are stackless, i.e. they only allocate their own frame and use the callers stack on resumption. Using our previous example:

fictional_eager_coro_type<int> example()
{
    co_yield 0;
    co_yield 1;
}

void nested_resume(fictional_eager_coro_type<int>& f)
{
    f.resume();
}

int main()
{
    auto f = example();
    nested_resume(f);
    f.reenter();
    return 0;
}

This will yield a call stack similar to this:

main()
  f$example()
  nested_resume()
    f$example()
  f$example()
Diagram

The same applies if a coroutine gets moved accross threads.

Lazy & eager

Coroutines are lazy if they only start execution of its code after it gets resumed, while an eager one will execute right-away until its first suspension point (i.e. a co_await, co_yield or co_return expression.)

lazy_coro co_example()
{
    printf("Entered coro\n");
    co_yield 0;
    printf("Coro done\n");
}

int main()
{
    printf("enter main\n");
    auto lazy = co_example();
    printf("constructed coro\n");
    lazy.resume();
    printf("resumed once\n");
    lazy.resume();
    printf("resumed twice\n");
    return 0;
}

Which will produce output like this:

enter main
constructed coro
Entered coro
resumed once
Coro Done
resumed twice
Diagram

Whereas an eager coro would look like this:

eager_coro co_example()
{
    printf("Entered coro\n");
    co_yield 0;
    printf("Coro done\n");
}

int main()
{
    printf("enter main\n");
    auto lazy = co_example();
    printf("constructed coro\n");
    lazy.resume();
    printf("resumed once\n");
    return 0;
}

Which will produce output like this:

enter main
Entered coro
constructed coro
resume once
Coro Done
Diagram