Aug 3, 2025

[C++] coroutine cheat sheet

Key components:

  • Return type
  • Promise type
  • Awaitable type
  • std::coroutine_handle<>

Clang Attributes:

task<int> coro(const int& a) { co_return a + 1; }
task<int> dangling_refs(int a) {
  // `coro` captures reference to a temporary. `foo` would now contain a dangling reference to `a`.
  auto foo = coro(1);
  // `coro` captures reference to local variable `a` which is destroyed after the return.
  return coro(a);
}

template <typename T> struct [[clang::coro_return_type, clang::coro_lifetimebound]] Task {
  using promise_type = some_promise_type;
};

Task<int> coro(const int& a) { co_return a + 1; }
[[clang::coro_wrapper]] Task<int> coro_wrapper(const int& a, const int& b) {
  return a > b ? coro(a) : coro(b);
}
Task<int> temporary_reference() {
  auto foo = coro(1); // warning: capturing reference to a temporary which would die after the expression.

  int a = 1;
  auto bar = coro_wrapper(a, 0); // warning: `b` captures reference to a temporary.

  co_return co_await coro(1); // fine.
}
[[clang::coro_wrapper]] Task<int> stack_reference(int a) {
  return coro(a); // warning: returning address of stack variable `a`.
}

Important performance to avoid temporary object being created during pass by value
but resides in the register
https://vsdmars.blogspot.com/2021/01/c-trivialabi-and-borrow-ownership.html

Attribute trivial_abi has no effect in the following cases:
  • The class directly declares a virtual base or virtual methods.
  • Copy constructors and move constructors of the class are all deleted.
  • The class has a base class that is non-trivial for the purposes of calls.
  • The class has a non-static data member whose type is non-trivial for the purposes of calls, which includes:
    • classes that are non-trivial for the purposes of calls
    • __weak-qualified types in Objective-C++
    • arrays of any of the above

Concept:

`co_yield value` is a syntax sugar for `co_await awaitable`.
thus value is stored in promise_type instance directly instead of having to go through
`Awaitable{value}` and store the value into promise_type instance through `Awaitable::await_suspend(handler)`

Caller could retrieve the value through
`ReturnType::handler_::promise.value_;`

std::suspend_always {
  // always suspend.
  bool await_ready() { return false;}
  // noop
  void await_suspend()
  // noop, the value is stored in promise_type.value_.
  void await_resume()
} // https://en.cppreference.com/w/cpp/coroutine/suspend_always.html

std::suspend_never {
  // never suspend.
  bool await_ready() { return true;}
  // noop
  void await_suspend()
  // noop, the value is stored in promise_type.value_.
  void await_resume()
}; // https://en.cppreference.com/w/cpp/coroutine/suspend_never.html

// Controls caller's behavior. Thus await_suspend is passing in caller's
// coroutine handler. However, the Awaitable instance is created by the
// callee, which it could store the callee's coroutine handler.
strict Awaitable {
  T value_;
  bool await_ready() { return false;}
  void await_suspend(std::coroutine_handle<promise_type> handler) {
    // could store the result into handler.promise().RESULT.
    //    or store the value_ into handler.promise().RESULT.
    // Handler can be passed into new thread.
    // When handler calls .resume(), it starts execute from where previous
    // suspended.
    // These await_* functions controlls how the caller, i.e. not the callee that
    // provides this Awaitable but the caller, behave.
    // if returns false, indicates the caller will continue and
    // await_resume is called immidiately and the result is returned to the caller.
    // if returns true, the caller is suspended and the callee only resume
    // if callee's handler::resume() is called. otherwise, callee and caller both
    // are suspended and return back to main()/root caller function(i.e. the caller function
    // that is not a coroutine.
  }

  // handler.resume() from Awaitable suspend comes here.
  // can return value to the caller inside the coroutine.
  // the value can be obtained from handler.promise().RESULT.
  void await_resume();
  T await_resume() {return T{}; };

  Awaitable(T value) : value_(value) {}
};



struct ReturnType {
  struct promise_type {
    T value_;
    promise_type(T...); // optional

    ReturnType get_return_object() {
      return {};
      // or
      return {coroutine::from_promise(*this)};
    };

    std::suspend_always initial_suspend() {}

    template<std::convertible_to<T> From> // C++20 concept
        std::suspend_always yield_value(From&& from) {
            value_ = std::forward<From>(from); // caching the result in promise
            return {};
    }

    // above start; below shutdown

    void return_value(T/T&&/const T&); // store value T inside the ReturnType, caller retrieve the value from ReturnType. 
    void return_void();

    void unhandled_exception();
    
    // 1) if returns suspend_always; the caller has the chance to control how coroutine is going
    //     to be clean-up, e.g. release lock, release resources etc. and then call .destroy()
    //     manually, usually in the ReturnType's destructor. After .destroy() is called,
    //     std::coroutine_handle::operator bool continue to return true, this is due
    //     to coroutine_handle does initial with a coroutine(that it points to).
    //     And If std::coroutine_handle::destroy()
    //     is called, std::coroutine_handle::done() will have undefined behavior and should not be called.
    // 2) if returns suspend_never; the coroutine is destroyed immediately, and calling 
    //    std::coroutine_handle::done() is UB since frame is destroyed.
    // 3) if suspend_always, the coroutine is suspended and std::coroutine_handle::done() is True.
    std::suspend_always final_suspend() noexcept;
  };

  ReturnType(std::coroutine_handle<promise_type> handler) : handler_(handler) {}

  ReturnType() = default;
};

No comments:

Post a Comment

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