Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
aria::async::CoroutineScope Class Reference

#include <scope.hpp>

Inheritance diagram for aria::async::CoroutineScope:
[legend]

Classes

struct  JoinAwaiter
 Awaitable resumed when in-flight count reaches zero. More...

Public Member Functions

 CoroutineScope ()
 Default scope — fully independent, no parent linkage.
 CoroutineScope (CancellationToken parent)
 Child scope: cancelling parent cancels this scope as well.
 ~CoroutineScope ()
 CoroutineScope (const CoroutineScope &)=delete
CoroutineScopeoperator= (const CoroutineScope &)=delete
 CoroutineScope (CoroutineScope &&)=delete
CoroutineScopeoperator= (CoroutineScope &&)=delete
CancellationToken token () const noexcept
bool is_cancelled () const noexcept
std::size_t inflight_count () const noexcept
 Number of coroutines currently in flight (launched but not yet returned).
void cancel () noexcept
 Request cancellation.
bool cancel_and_join (std::chrono::milliseconds timeout=std::chrono::milliseconds{5000}) noexcept
 Synchronously: cancel + wait for all in-flight coroutines to finish, with a bounded timeout (default 5 s).
JoinAwaiter join () noexcept
 Awaitable equivalent of cancel_and_join() — request cancellation then suspend the calling coroutine until in-flight count reaches zero.
JoinAwaiter join_existing () noexcept
 Like join() but does NOT request cancellation first — it just waits for whatever is currently in flight to complete naturally.
template<typename Fn>
void launch (Fn &&factory)
 Launch a coroutine factory Task<void> fn(CancellationToken).
void launch_simple (Task< void > task)
 Convenience overload for a fully-formed Task<void> whose body already captures the cancellation token (or doesn't need one).

Constructor & Destructor Documentation

◆ CoroutineScope() [1/4]

aria::async::CoroutineScope::CoroutineScope ( )
inline

Default scope — fully independent, no parent linkage.

◆ CoroutineScope() [2/4]

aria::async::CoroutineScope::CoroutineScope ( CancellationToken parent)
inlineexplicit

Child scope: cancelling parent cancels this scope as well.

The child is otherwise independent (cancelling the child does NOT propagate up to the parent, matching Kotlin semantics).

◆ ~CoroutineScope()

aria::async::CoroutineScope::~CoroutineScope ( )
inline

◆ CoroutineScope() [3/4]

aria::async::CoroutineScope::CoroutineScope ( const CoroutineScope & )
delete

◆ CoroutineScope() [4/4]

aria::async::CoroutineScope::CoroutineScope ( CoroutineScope && )
delete

Member Function Documentation

◆ operator=() [1/2]

CoroutineScope & aria::async::CoroutineScope::operator= ( const CoroutineScope & )
delete

◆ operator=() [2/2]

CoroutineScope & aria::async::CoroutineScope::operator= ( CoroutineScope && )
delete

◆ token()

CancellationToken aria::async::CoroutineScope::token ( ) const
inlinenodiscardnoexcept

◆ is_cancelled()

bool aria::async::CoroutineScope::is_cancelled ( ) const
inlinenodiscardnoexcept

◆ inflight_count()

std::size_t aria::async::CoroutineScope::inflight_count ( ) const
inlinenodiscardnoexcept

Number of coroutines currently in flight (launched but not yet returned).

Useful for tests and diagnostics.

◆ cancel()

void aria::async::CoroutineScope::cancel ( )
inlinenoexcept

Request cancellation.

Non-blocking: in-flight coroutines will observe the cancellation at their next probe / co_await.

◆ cancel_and_join()

bool aria::async::CoroutineScope::cancel_and_join ( std::chrono::milliseconds timeout = std::chrono::milliseconds{5000})
inlinenoexcept

Synchronously: cancel + wait for all in-flight coroutines to finish, with a bounded timeout (default 5 s).

On timeout, a leak diagnostic is emitted via the async error sink and the function returns; the scope is left with a non-zero inflight count. Returns true if everyone drained, false on timeout.

◆ join()

JoinAwaiter aria::async::CoroutineScope::join ( )
inlinenodiscardnoexcept

Awaitable equivalent of cancel_and_join() — request cancellation then suspend the calling coroutine until in-flight count reaches zero.

Does NOT have a timeout; intended for cooperative shutdown from inside another coroutine.

◆ join_existing()

JoinAwaiter aria::async::CoroutineScope::join_existing ( )
inlinenodiscardnoexcept

Like join() but does NOT request cancellation first — it just waits for whatever is currently in flight to complete naturally.

◆ launch()

template<typename Fn>
void aria::async::CoroutineScope::launch ( Fn && factory)
inline

Launch a coroutine factory Task<void> fn(CancellationToken).

The returned task is wrapped, accounted for in inflight_count(), and any unhandled exception (other than OperationCancelled) is reported via the async error sink.

IMPORTANT — lambda-captures lifetime contract:

factory is almost always a lambda whose body is itself a coroutine ([caps](CancellationToken tok) -> Task<void> { ... }). A C++ coroutine that lives inside a lambda body does NOT copy the lambda's captures into its own coroutine frame — instead, it stores this and reads captures through it. So if we let factory itself live only as long as the launch() call, every capture (e.g. shared_ptr<atomic<bool>> used to observe state from the test) is destroyed the moment launch() returns, while the user coroutine is still parked. Subsequent capture access from inside the body is undefined behaviour. The exact symptom observed in the wild was MSVC release builds producing stopped->store(true) writes that the main thread never read back, in the parent->child cancellation test.

The fix is to host factory inside a tiny wrapper coroutine (launch_owner_coro_). Coroutine parameters (unlike lambda captures) are by-value-copied into the coroutine frame, so the wrapper frame owns factory for the entire lifetime of the user task. The user lambda's this pointer therefore remains valid until the user task completes.

Cost note: each launch() therefore performs two coroutine frame heap allocations — one for launch_owner_coro_, one for the user task. Halo (heap-allocation-elision optimisation) is not eligible here because both frames outlive the call (they detach into start_detached_()). This is the unavoidable price of routing factory through a coroutine parameter slot; in every benchmarked workload it is dominated by the user task's own work and never becomes the bottleneck. If a caller genuinely needs the wrapper-free path (e.g. very high-frequency fire-and-forget launches with no captures), launch_simple() is the documented escape hatch.

Fn is forwarded perfectly so that move-only callables (e.g. lambdas capturing std::unique_ptr) work, and lvalue callables are copied at the call site exactly once into the wrapper coroutine frame.

◆ launch_simple()

void aria::async::CoroutineScope::launch_simple ( Task< void > task)
inline

Convenience overload for a fully-formed Task<void> whose body already captures the cancellation token (or doesn't need one).

Caller-owned capture lifetime contract:

Unlike launch(), this overload does NOT host the originating callable inside an owner coroutine — it accepts the resulting Task<void> directly. If the caller produced that task by invoking a lambda whose body is itself a coroutine (auto t = [caps]() -> Task<void> { ... }();), the lambda is a temporary that dies at the end of the enclosing full-expression, while the coroutine frame still holds a this pointer back into it — captures become dangling. Prefer launch(factory) for the lambda-factory case; it owns the lambda for you. launch_simple() is the escape hatch for callers that have already arranged capture lifetimes themselves (e.g. by passing state through coroutine parameters, or by anchoring the lambda in a longer-lived storage).


The documentation for this class was generated from the following file: