Sep 28, 2025

[C++] coroutine cheat sheet - 2

Reference:
[C++] coroutine cheat sheet - 1
https://reductor.dev/cpp/2023/08/10/the-downsides-of-coroutines.html#how-normal-functions-and-the-stack-work

Rule

  1. Do not resume caller's handler inside a mem_fn that returns value/handler.
    otherwise the return will be suspended in the callee.
    reference:
    https://lewissbaker.github.io/2020/05/11/understanding_symmetric_transfer
    A pattern, however, the caller's awaitable::await_suspend() could call callee's handler to resume,
    while callee's promise::final_suspend() could call caller handler to resume() and back to
    caller's awaitable::await_suspend().
  2. Control the suspend from mem_fn that it's signature returns awaitable type.
  3. Store {yeild values/caller's handler} inside the promise instance.
  4. Make sure promise::final_suspend() is run before caller resume.
    This indicates, the caller::handler::resume() can only be called
    inside the promise::final_suspend() returned awaitable::await_suspend() and awaitable::await_ready() should return false to trigger the run of awaitable::await_suspend()
  5. If promise::final_suspend() returns std::suspend_never{}, the coroutine heap is destroyed automatically/immidiately.
    If promise::final_suspend() returns other then std::suspend_never{} and
    has the awaitable::await_ready() returns false, the user defined coroutine
    handler's destructor should call detroy() manually to release the heap.
  6. Thread local variables should not be used in coroutines to prevent buggy logic.
    https://rules.sonarsource.com/cpp/RSPEC-6367
Coroutine defined as:
task<void> func()
{
  int mylocal = 10;
  co_return;
}
Compiler generated coroutine code would be like:
// Struct representing the coroutine state
struct func_frame
{
  task<void>::promise_type __promise;
  int __step = 0;

  decltype(__promise.initial_suspend()) __initial_suspend_awaiter;
  decltype(__promise.final_suspend()) __final_suspend_awaiter;

  // Structure to hold local and temporary variables
  struct 
  {
    // Local and temporary variables reside here
    int mylocal;
  } local_and_temps;

  // Function to resume coroutine execution
  void resume()
  {
    switch(__step)
    {
      case 0:
        // co_await __promise.initial_suspend();
        __initial_suspend_awaiter = __promise.initial_suspend();
        if (!__initial_suspend_awaiter.await_ready())
        {
          __step = 1;
          __initial_suspend_awaiter.await_suspend();
          return;
        }
      case 1:
        __initial_suspend_awaiter.await_resume();
        // .. func body
        mylocal = 10;
        // co_return
        __promise.return_void();
        // co_await __promise.final_suspend();
        __final_suspend_awaiter = __promise.final_suspend();
        if (!__final_suspend_awaiter.await_ready())
        {
          __final_suspend_awaiter.await_suspend();
          return;
        }
        delete this;
    }
  }
};

// Coroutine function transformed into a coroutine frame
task<void> func()
{
  func_frame * frame = task<void>::promise_type::operator new(func_frame);
  task<void> ret = frame->__promise.get_return_object()
  frame->resume();
  return ret;
}
Which involves multiple function calls:
  • func()
  • promise_type::operator new()
  • promise_type::promise_type()
  • promise_type::get_return_value()
  • promise_type::initial_suspend()
  • initial_suspend::initial_suspend()
  • initial_suspend::await_ready()
  • initial_suspend::await_suspend() [opt]
  • initial_suspend::await_resume()
  • promise_type::return_void()
  • final_suspend::final_suspend()
  • final_suspend::await_ready()
  • final_suspend::await_suspend() [opt]
  • promise_type::operator delete()

Code explains:
#include <coroutine>
#include <iostream>

/**
Rule:
1. Do not resume caller's handler inside a mem_fn that returns value/handler.
  otherwise the return will be suspended in the callee.
2. Control the suspend from mem_fn that it's signature returns awaitable type.
3. store {yeild values/caller's handler} inside the promise instance.
4. make sure promise::final_suspend() is run before caller resume.
  this indicates, the caller::handler::resume() can only be called
  inside the promise::final_suspend() returned awaitable::await_suspend() and
  awaitable::await_ready() should return false to trigger the run of
  awaitable::await_suspend()
5. If promise::final_suspend() returns std::suspend_never{}, the coroutine heap
  is destroyed automatically.
  If promise::final_suspend() returns other then std::suspend_never{} and
  has the awaitable::await_ready() returns false, the user defined coroutine
handler's destructor should call detroy() manually to release the heap.
*/
using namespace std;

enum class TaskType : char {
  foo,
  bar,
};

template <TaskType ttype> struct Task;

// promise_type
template <TaskType ttype> struct promise_type {
  Task<ttype> get_return_object();
  std::suspend_always initial_suspend();

  auto final_suspend() noexcept;

  void return_value(int);
  void unhandled_exception();
  TaskType get_ttype() { return ttype; }
  int ret_val;
  std::coroutine_handle<promise_type<TaskType::bar>> promise_type_handle_;
};
// End promise_type

template <TaskType ttype>
struct Task : std::coroutine_handle<promise_type<ttype>> {
  using promise_type = promise_type<ttype>;
  // Awaitable interface definition
  bool await_ready() {
    std::cout << "await_ready: false\n";
    return false;
  }

  std::coroutine_handle<promise_type>
  await_suspend(std::coroutine_handle<::promise_type<TaskType::bar>> handler) {
    std::cout << "await_suspend receive handler pnted to type: "
              << (int)handler.promise().get_ttype()
              << " and this task's type: " << (int)this->promise().get_ttype()
              << "\n";

    // using this due to 13.4.2 Dependent Base Classes
    this->promise().promise_type_handle_ = handler;
    return *this;
  }

  int await_resume() {
    std::cout << "await_resume; type: " << (int)ttype << "\n";
    return this->promise().ret_val;
  }
  // End Awaitable interface definition

  ~Task() {
    std::cout << "Task destructor called, type: " << (int)ttype << "\n";
    this->destroy();
  }

  TaskType get_ttype() { return ttype; }

  // If has default Task constructor, this `return
  // {Task<ttype>::from_promise(*this)};` won't compile.
  // Task() { std::cout << "task construct type: " << (int)ttype << "\n"; }

}; // Task

// promise_type mem_fn definition
template <TaskType ttype> Task<ttype> promise_type<ttype>::get_return_object() {
  std::cout << "get_return_object type: " << (int)ttype << "\n";
  return {Task<ttype>::from_promise(*this)};
};

template <TaskType ttype>
std::suspend_always promise_type<ttype>::initial_suspend() {
  std::cout << "initial_suspend always\n";
  return {};
}

template <TaskType ttype> auto promise_type<ttype>::final_suspend() noexcept {
  std::cout << "final_suspend, this promise type: " << (int)ttype << "\n";
  // Bar's handler
  // if (promise_type_handle_) {
  //   std::cout << "resume bar's handler\n";
  //   promise_type_handle_.resume();
  // }
  struct tmp_awaitable {
    bool await_ready() noexcept {
      std::cout << "tmp_awaitable await_ready false\n";
      return false;
    }

    std::coroutine_handle<> await_suspend(
        std::coroutine_handle<::promise_type<TaskType::foo>> handler) noexcept {
      std::cout << "tmp_awaitable await_suspend\n";
      if (handler.promise().promise_type_handle_) {
        return handler.promise().promise_type_handle_;
      }
      std::cout << "tmp_awaitable await_suspend after resume\n";
      return std::noop_coroutine();
    }

    void await_resume() noexcept {
      std::cout << "tmp_awaitable await_resume\n";
    }
  };

  if constexpr (ttype == TaskType::foo) {
    std::cout << "final_suspend type: " << (int)ttype << " suspend never\n";
    // return std::suspend_never{};
    return tmp_awaitable{};
  } else {
    std::cout << "final_suspend type: " << (int)ttype << " suspend always\n";
    return std::suspend_always{};
  }
}

template <TaskType ttype> void promise_type<ttype>::return_value(int val) {
  std::cout << "return_val: " << val << "\n";
  ret_val = val;
  // promise_type_handle_.resume();
}

template <TaskType ttype> void promise_type<ttype>::unhandled_exception() {
  std::cout << "unhandled_exception\n";
}
// End promise_type mem_fn definition

Task<TaskType::foo> foo() {
  std::cout << "foo func start\n";
  co_return 42;
}

Task<TaskType::bar> bar() {
  std::cout << "bar func start\n";
  // co_return;
  // co_await foo();
  int val = co_await foo(); // Foo's task destructs here after co_await returns;
  std::cout << "bar func end val: " << val << "\n";
}

int main() {
  std::cout << "call bar\n";
  auto btask = bar();
  std::cout << "get btask and resume bar handle: " << (int)btask.get_ttype()
            << "\n";
  btask.resume();
  std::cout << "main-end\n";
  /*
        start call bar
        initial_suspend
        main-end
   */
}

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.