Search code examples
c++c++20c++-coroutinestack-unwinding

How to detect stack unwinding in C++20 coroutines?


The typical advice in C++ is to detect stack unwinding in the destructor using std::uncaught_exceptions(), see the example from https://en.cppreference.com/w/cpp/error/uncaught_exception :

struct Foo {
    int count = std::uncaught_exceptions();
    ~Foo() {
        std::cout << (count == std::uncaught_exceptions()
            ? "~Foo() called normally\n"
            : "~Foo() called during stack unwinding\n");
    }
};

But this advice looks no longer applicable to C++20 coroutines, which can be suspended and resumed including during stack unwinding. Consider the following example:

#include <coroutine>
#include <iostream>

struct ReturnObject {
  struct promise_type {
    ReturnObject get_return_object() { return { std::coroutine_handle<promise_type>::from_promise(*this) }; }
    std::suspend_always initial_suspend() { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    void unhandled_exception() {}
    void return_void() {}
  };
  std::coroutine_handle<promise_type> h_;
};

struct Foo {
    int count = std::uncaught_exceptions();
    Foo() { std::cout << "Foo()\n"; }
    ~Foo() {
        std::cout << (count == std::uncaught_exceptions()
            ? "~Foo() called normally\n"
            : "~Foo() called during stack unwinding\n");
    }
};

struct S
{
    std::coroutine_handle<ReturnObject::promise_type> h_;
    ~S() { h_(); }
};

int main()
{
  auto coroutine = []() -> ReturnObject { Foo f; co_await std::suspend_always{}; };
  auto h = coroutine().h_;

  try
  {
      S s{ .h_ = h };
      std::cout << "Exception being thrown\n";
      throw 0; // calls s.~S() during stack unwinding
  }
  catch( int ) {}
  std::cout << "Exception caught\n";

  h();
  h.destroy();
}

It uses the same class Foo inside the coroutine, which is destructed normally (not due to stack unwinding during exception), but still prints:

Exception being thrown
Foo()
Exception caught
~Foo() called during stack unwinding

Demo: https://gcc.godbolt.org/z/Yx1b18zT9

How one can re-design class Foo to properly detect stack unwinding in coroutines as well?


Solution

  • The archetypal reason for wanting to know if a function is being executed due to stack unwinding is for something like rolling back a database transaction. So the situation looks rather like this:

    Your function does some database work. It creates a database transaction governed by a RAII object. That object is on the function's stack (either directly or indirectly as a subobject of some other stack object). You do some stuff, and when that RAII object leaves the stack, the database transaction should commit or rollback, depending on whether it left the stack normally or because an exception passed through the function itself respectively.

    This is all pretty neat and tidy. There is no explicit cleanup code needed in the function itself.

    What does this mean for a coroutine? That becomes exceedingly complicated, because a coroutine can be terminated for reasons outside of its own execution.

    For a normal function, it either completes or throws an exception. If such a function fails, it happens internally to the function. Coroutines don't work like that. Between suspend points, the code that schedules the resumption of the coroutine might itself fail.

    Consider asynchronous file loading. You pass a continuation function to the file reader, and the continuation will be given the file data as it gets read to process it. Partially through this process, a file read error happens. But that happens in the external code that's accessing the file, not the continuation function that is consuming it.

    So the external code needs to tell the consuming function that an error happened and it should abort its process. This cannot happen via an exception (at least not by default); the interface between these two pieces of code must have a mechanism to transmit that the process failed. There are ways to have this interface actually throw an exception within the continuation function itself (ie: the continuation gets some object that it calls to access the currently read data, and it throws if a read error happened), but that is still a cooperative mechanism.

    It doesn't happen by itself.

    So even if you could solve this problem in a coroutine, you would still need to account for cases when a coroutine needs to terminate for reasons outside of an exception thrown from within. Since you're going to need explicit code to do cleanup/rollbacks/etc anyway, there's little point in relying on purely RAII mechanisms to do this.

    To more directly answer the question, if you still want to do this, you need to treat the code between suspend points as if they were their own functions. Each suspend point is effectively a separate function call, with its own exception count and so forth.

    So either a RAII object lives entirely between suspend points, or you need to update the exception count every time a suspend point starts.