I'm writing some test code using Boost.Asio with C++20 coroutines.
Working version (manual cleanup, non-RAII)
The following code works as expected. Cleanup is always called, even when an exception is thrown from
test_body().#include <iostream> #include <boost/asio.hpp> namespace asio = boost::asio; asio::awaitable<void> test_setup() { std::cout << "setup" << std::endl; co_return; } asio::awaitable<void> test_body() { std::cout << "body" << std::endl; throw 1; // Simulate exception in test code co_return; } asio::awaitable<void> test_cleanup() { std::cout << "cleanup" << std::endl; co_return; } asio::awaitable<void> test_launcher() { co_await test_setup(); try { co_await test_body(); } catch (...) { std::cout << "caught exception" << std::endl; } // Always perform cleanup co_await test_cleanup(); co_return; } int main() { asio::io_context ioc; asio::co_spawn( ioc.get_executor(), test_launcher, asio::detached ); ioc.run(); }Output:
setup body caught exception cleanupRAII-style version (problematic)
I want to remove the
try-catchfromtest_launcher()and instead handle exceptions using the third argument ofco_spawn. Here's what I tried:asio::co_spawn( ioc.get_executor(), test_launcher, [](std::exception_ptr ep) { if (ep) { try { std::rethrow_exception(ep); } catch (...) { std::cout << "caught exception" << std::endl; } } } );However, this means that
test_cleanup()is not executed whentest_body()throws an exception. I tried to work around this using a scope guard.Scope guard workaround (but still not ideal)
I attempted to implement RAII-style cleanup using a
shared_ptrwith a custom deleter. Since the deleter cannot beco_await-ed, I spawn a detached coroutine from inside the deleter.asio::awaitable<void> test_launcher() { co_await test_setup(); auto exe = co_await asio::this_coro::executor; std::shared_ptr<void> scope_guard{ nullptr, [exe](void*) { std::cout << "scope_guard called" << std::endl; asio::co_spawn( exe, test_cleanup, // this is an awaitable function asio::detached // can't use use_awaitable inside non-awaitable context ); } }; co_await test_body(); // throws co_return; }Output:
setup body scope_guard called cleanup begin caught exception cleanup end
Problems:
- The scope guard (
shared_ptrdeleter) cannot directlyco_await, so I had toco_spawnthe cleanup instead. - Since the spawned cleanup is detached, I have no way to wait for it to finish.
- If
test_cleanup()is long-running, it may still be executing after the main coroutine ends.
Is there a recommended way to perform RAII-style cleanup for awaitable functions in Boost.Asio coroutines?
That is, I'd like cleanup to always run, even when exceptions are thrown, without manually writing try-catch in every coroutine.