diff --git a/include/tmc/all_headers.hpp b/include/tmc/all_headers.hpp index 477dcb27..86936690 100644 --- a/include/tmc/all_headers.hpp +++ b/include/tmc/all_headers.hpp @@ -32,6 +32,7 @@ #include "tmc/qu_spsc_bounded.hpp" // IWYU pragma: export #include "tmc/qu_spsc_unbounded.hpp" // IWYU pragma: export #include "tmc/rw_lock.hpp" // IWYU pragma: export +#include "tmc/select.hpp" // IWYU pragma: export #include "tmc/semaphore.hpp" // IWYU pragma: export #include "tmc/spawn.hpp" // IWYU pragma: export #include "tmc/spawn_func.hpp" // IWYU pragma: export diff --git a/include/tmc/detail/concepts_awaitable.hpp b/include/tmc/detail/concepts_awaitable.hpp index 50e857d1..64626b77 100644 --- a/include/tmc/detail/concepts_awaitable.hpp +++ b/include/tmc/detail/concepts_awaitable.hpp @@ -21,8 +21,7 @@ template struct await_resume_t_impl { using type = unknown_t; }; template struct await_resume_t_impl { - using type = - std::remove_reference_t().await_resume())>; + using type = std::remove_reference_t().await_resume())>; }; /// end await_resume_t_impl @@ -39,9 +38,7 @@ template struct unknown_awaitable_traits { template static decltype(auto) guess_awaiter(T&& value) { if constexpr (requires { static_cast(value).operator co_await(); }) { return static_cast(value).operator co_await(); - } else if constexpr (requires { - operator co_await(static_cast(value)); - }) { + } else if constexpr (requires { operator co_await(static_cast(value)); }) { return operator co_await(static_cast(value)); } else { return static_cast(value); @@ -97,6 +94,9 @@ using result_type = typename (result type of `co_await YourAwaitable;`); // Implementing this is OPTIONAL; if unimplemented, each awaitable will be // wrapped in a task that will automagically restore its executor and priority // before returning, thus preventing errors out-of-the-box. +// +// Setting the reference category of self_type (& or &&) controls whether this is an +rvalue-only or lvalue-only awaitable. static awaiter_type get_awaiter(self_type& Awaitable) { return awaiter_type(Awaitable); @@ -116,6 +116,9 @@ static constexpr configure_mode mode = COROUTINE; // If set to ASYNC_INITIATE, you must define this function, which will be // called to initiate the async process. The current TMC executor and priority // will be passed in, but they are not required to be used. +// +// Setting the reference category of Awaitable (& or &&) controls whether this is an +rvalue-only or lvalue-only awaitable. static constexpr configure_mode mode = ASYNC_INITIATE; static void async_initiate( @@ -147,8 +150,7 @@ static void set_flags(Awaitable& YourAwaitable, size_t Flags); */ template -using get_awaitable_traits = - awaitable_traits>; +using get_awaitable_traits = awaitable_traits>; template using awaitable_result_t = @@ -185,11 +187,9 @@ template struct awaitable_traits { struct AwaitTagNoGroupCoAwait {}; template -concept HasAwaitTagNoGroupCoAwait = - std::is_base_of_v; +concept HasAwaitTagNoGroupCoAwait = std::is_base_of_v; -template -struct awaitable_traits { +template struct awaitable_traits { static constexpr configure_mode mode = WRAPPER; static decltype(auto) get_awaiter(Awaitable&& awaitable) noexcept { @@ -211,8 +211,7 @@ template concept HasAwaitTagNoGroupCoAwaitLvalue = std::is_base_of_v; -template -struct awaitable_traits { +template struct awaitable_traits { static constexpr configure_mode mode = WRAPPER; static decltype(auto) get_awaiter(Awaitable& awaitable) noexcept { @@ -247,8 +246,7 @@ template struct range_iter { /// Given T&& -> holds T&& if T is not move-constructible template using forward_awaitable = std::conditional_t< - std::is_rvalue_reference_v && - std::is_move_constructible_v>, + std::is_rvalue_reference_v && std::is_move_constructible_v>, std::decay_t, T&&>; /// Must be defined by each TMC executor. No default implementation is provided. diff --git a/include/tmc/select.hpp b/include/tmc/select.hpp new file mode 100644 index 00000000..e3327fab --- /dev/null +++ b/include/tmc/select.hpp @@ -0,0 +1,330 @@ +// Copyright (c) 2023-2026 Logan McDougall +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE or copy at http://www.boost.org/LICENSE_1_0.txt) + +#pragma once + +#include "tmc/detail/compat.hpp" +#include "tmc/detail/concepts_awaitable.hpp" // IWYU pragma: keep +#include "tmc/spawn_tuple.hpp" +#include "tmc/task.hpp" +#include "tmc/traits.hpp" + +#include +#include +#include +#include +#include + +namespace tmc { +namespace detail { +// Marker used as the `Canceller` of a `tmc::cancellable` when the awaitable is +// its own cancellation handle (the single-argument `tmc::cancellable` +// constructor). It is empty; with TMC_NO_UNIQUE_ADDRESS on the member, it occupies no +// storage. +struct cancel_self_t {}; + +// A nullary canceller that holds a cancellation target and invokes its +// `.cancel()` method, forwarding whatever that method returns. `Target` is +// instantiated as `forward_awaitable`, so the target is owned by value when +// it was passed as a movable rvalue, and held by reference (lvalue or +// non-movable rvalue) otherwise. Produced by the object-taking constructor +// (form 2) of `tmc::cancellable`. The forwarded result lets `select()` detect an +// awaitable `.cancel()` (an async cancel) and await it. +template struct cancel_caller { + Target target; + decltype(auto) operator()() { return target.cancel(); } +}; + +// A trivial awaiter used by `select()` only in its async cancel branch for any sync +// canceller mixed in. The fold expression cannot handle mixed await / no-await, so we +// must provide an awaitable object for syntactic compatibility. +struct cancel_noop : tmc::detail::AwaitTagNoGroupAsIs { + static constexpr bool await_ready() noexcept { return true; } + static void await_suspend(std::coroutine_handle<>) noexcept {} + static void await_resume() noexcept {} +}; + +// Forward-declared here so `tmc::cancellable` can grant it friendship. +template consteval bool canceller_is_async(); + +} // namespace detail + +/// Pairs an `Awaitable` with a `Canceller` for use with `tmc::select()`. +/// +/// Construct one with class template argument deduction - `tmc::cancellable(...)` +/// selects the correct specialization automatically. Three forms are supported: +/// +/// 1. `tmc::cancellable(awaitable, canceller)` where `canceller` is a nullary functor +/// that cancels the operation when invoked. It may return void (a sync cancel) or +/// an awaitable (an async cancel, which `select()` awaits). The canceller is +/// stored by value and invoked only for losers, so an awaitable-returning canceller +/// builds its awaitable only when the cancel is actually needed. +/// ``` +/// // sync cancel +/// tmc::cancellable(timer.async_wait(tmc::aw_asio), [&]{ timer.cancel(); }) +/// +/// // async cancel via a functor lazily returning an awaitable +/// tmc::cancellable(op.async_do(), [&]{ return op.async_cancel(); }) +/// +/// // async cancel via a functor lazily returning a task +/// tmc::cancellable(op.async_do(), [&]() -> tmc::task { +/// co_await op.async_cancel(); +/// }) +/// ``` +/// +/// 2. `tmc::cancellable(awaitable, object)` where `object` exposes a `.cancel()` method. +/// The `.cancel()` method may be sync or async (awaitable). +/// ``` +/// // sync cancel - timer.cancel() runs syncly (an Asio I/O object) +/// tmc::cancellable(timer.async_wait(tmc::aw_asio), timer) +/// +/// // async cancel - op.cancel() returns an awaitable, which select() awaits +/// tmc::cancellable(op.async_do(), op) +/// ``` +/// +/// 3. `tmc::cancellable(object)` where `object` is itself both the awaitable and +/// the cancellation handle: it can be co_awaited and also exposes a `.cancel()` +/// method. The `.cancel()` method can be sync or async (awaitable); `select()` will +/// automatically await it if needed. This constructor only accepts lvalues or non-movable +/// rvalues. Movable rvalues are rejected to prevent lifetime issues. +/// ``` +/// // sync cancel - op.cancel() runs syncly +/// tmc::cancellable(op) +/// +/// // async cancel - op2.cancel() returns an awaitable, which select() awaits +/// tmc::cancellable(op2) +/// ``` +/// +/// In every form, the cancellation action must be safe to run even +/// after the operation has already completed (it may race with completion) and +/// should not throw. `select()` runs the canceller of each losing awaitable +/// exactly once. +/// +/// Value category is forwarded throughout: a movable rvalue is owned by value; +/// an lvalue or non-movable rvalue is borrowed (held by reference) and must +/// outlive the `tmc::select()` call. Constructor CTAD handles this automatically. +template class cancellable { + Awaitable awaitable; + // Empty cancellers (a captureless lambda, or `cancel_self_t`) cost no storage. + TMC_NO_UNIQUE_ADDRESS Canceller canceller; + + template + friend tmc::task::result_type>...>> + select(tmc::cancellable... Pairs); + + template + friend consteval bool tmc::detail::canceller_is_async(); + +public: + /// `tmc::cancellable(awaitable, canceller)` where `canceller` is a nullary functor + /// that cancels the operation when invoked. It may return void (a sync cancel) + /// or an awaitable (an async cancel, which `select()` awaits). The canceller is + /// stored by value and invoked only for losers, so an awaitable-returning canceller + /// builds its awaitable only when the cancel is actually needed. + /// ``` + /// // sync cancel + /// tmc::cancellable(timer.async_wait(tmc::aw_asio), [&]{ timer.cancel(); }) + /// + /// // async cancel via a functor lazily returning an awaitable + /// tmc::cancellable(op.async_do(), [&]{ return op.async_cancel(); }) + /// + /// // async cancel via a functor lazily returning a task + /// tmc::cancellable(op.async_do(), [&]() -> tmc::task { + /// co_await op.async_cancel(); + /// }) + /// ``` + template + requires(tmc::traits::executable_kind_v> == + tmc::traits::executable_kind::CALLABLE) + cancellable(A&& Aw, C&& Cancel) + : awaitable(static_cast(Aw)), canceller(static_cast(Cancel)) {} + + /// `tmc::cancellable(awaitable, object)` where `object` exposes a `.cancel()` + /// method. The `.cancel()` method may be sync or async (awaitable). + /// ``` + /// // sync cancel - timer.cancel() runs syncly (an Asio I/O object) + /// tmc::cancellable(timer.async_wait(tmc::aw_asio), timer) + /// + /// // async cancel - op.cancel() returns an awaitable, which select() awaits + /// tmc::cancellable(op.async_do(), op) + template + requires(tmc::traits::executable_kind_v> == + tmc::traits::executable_kind::UNKNOWN && + requires(C& Obj) { Obj.cancel(); }) + cancellable(A&& Aw, C&& Obj) + : awaitable(static_cast(Aw)), canceller{static_cast(Obj)} {} + + /// `tmc::cancellable(object)` where `object` is itself both the awaitable and + /// the cancellation handle: it can be co_awaited and also exposes a `.cancel()` + /// method. The `.cancel()` method can be sync or async (awaitable); `select()` will + /// automatically await it if needed. This constructor only accepts lvalues or + /// non-movable rvalues. Movable rvalues would be unsafe here, and are deliberately + /// rejected. + template + requires(tmc::detail::is_awaitable> && + requires(A& Obj) { Obj.cancel(); } && + std::is_reference_v>) + explicit cancellable(A&& Obj) : awaitable(static_cast(Obj)), canceller{} {} +}; + +// Deduction guides pick the storage types that the matching constructor fills: +// the awaitable is forwarded (owned-by-value for a movable rvalue, by-reference +// otherwise); the canceller is stored as-is (form 1), wrapped in a +// `cancel_caller` (form 2), or replaced by the empty `cancel_self_t` (form 3). +// Their constraints mirror the constructors' so the right type is deduced. +template + requires( + tmc::traits::executable_kind_v> == + tmc::traits::executable_kind::CALLABLE + ) +cancellable(A&&, C&&) -> cancellable, std::decay_t>; + +template + requires( + tmc::traits::executable_kind_v> == + tmc::traits::executable_kind::UNKNOWN && + requires(C& Obj) { Obj.cancel(); } + ) +cancellable(A&&, C&&) -> cancellable< + tmc::detail::forward_awaitable, + tmc::detail::cancel_caller>>; + +template + requires( + tmc::detail::is_awaitable> && + requires(A& Obj) { Obj.cancel(); } && + std::is_reference_v> + ) +cancellable(A&&) + -> cancellable, tmc::detail::cancel_self_t>; + +namespace detail { +// Compile-time check whether the canceller of a `tmc::cancellable` is +// async (returns an awaitable rather than performing the cancellation directly). +template consteval bool canceller_is_async() { + using Pair = tmc::cancellable; + if constexpr (std::is_same_v) { + // self-cancel (form 3) + return tmc::traits::is_awaitable().awaitable.cancel())>; + } else { + // a callable canceller (form 1) or object with `cancel()` method (form 2) + return tmc::traits::is_awaitable().canceller())>; + } +} +} // namespace detail + +/// Awaits all of the provided awaitables and returns the result of the first +/// one to complete in a `std::variant`. The `index()` of the returned variant +/// indicates which awaitable completed first; its value holds that awaitable's +/// result. A void-returning awaitable's slot holds a `std::monostate`. +/// +/// Each awaitable must be paired with a canceller using `tmc::cancellable()`. +/// As soon as the first awaitable completes (the winner), the cancellers of all of the +/// others (losers) are run; cancellers that return awaitables (async cancel) are awaited. +/// Results from losers are discarded. +/// +/// If multiple awaitables complete simultaneously, the winner is the awaitable with the +/// lower index (leftmost parameter). All losers are cancelled, even those that may have +/// already completed. Therefore, it must be safe to cancel a completed awaitable. +/// +/// Implementation note: `select()` waits for cancelled losers to complete before +/// returning. This is required since wrapped operations borrow storage from this +/// coroutine frame, so they must all complete before it is destroyed. Consequently, a +/// canceller that cannot truly stop its operation will delay the return of `select()` +/// until that operation completes on its own. +template +tmc::task::result_type>...>> +select(tmc::cancellable... Pairs) { + static_assert(sizeof...(Awaitable) > 0, "select() requires at least one awaitable."); + static_assert( + sizeof...(Awaitable) < TMC_PLATFORM_BITS, + "select() supports at most 63 awaitables (31 on 32-bit platforms)." + ); + using variant_type = std::variant::result_type>...>; + constexpr size_t Count = sizeof...(Awaitable); + + // Forward each awaitable with its original value category so the awaitable's own + // lvalue/rvalue qualification is respected. + auto each = + tmc::spawn_tuple(static_cast(Pairs.awaitable)...).result_each(); + + // Wait for at least one operation to complete. + size_t winner = co_await each; + + // Move the winner's result into the variant slot for its index. + std::optional result; + auto storeWinner = [&](std::integral_constant) { + using VarElem = std::variant_alternative_t; + using Stored = std::remove_reference_t())>; + if constexpr (std::is_same_v) { + result.emplace(std::in_place_index, std::move(each.template get())); + } else { + // Non-default-constructible results are stored wrapped in std::optional. + result.emplace(std::in_place_index, std::move(*each.template get())); + } + }; + [&](std::index_sequence) { + ((winner == I ? (storeWinner(std::integral_constant{}), void()) : void()), + ...); + }(std::make_index_sequence{}); + + // Cancel every awaitable except the winner. The cancellation action of a loser is, per + // the cancellable forms: + // - self-cancel (form 3): `Pair.awaitable.cancel()`; + // - a callable canceller (form 1) or `cancel_caller` (form 2): + // `Pair.canceller()`. + // + // This is split on whether ANY canceller is async; a fold expression cannot contain a + // per-element `if constexpr` check, so it must go outside. + if constexpr (!(tmc::detail::canceller_is_async() || ...)) { + // All cancellations are sync. The fold expression is a plain call. + size_t idx = 0; + auto cancelSync = [](auto& Pair) { + if constexpr (std::is_same_v< + std::remove_reference_t, + tmc::detail::cancel_self_t>) { + Pair.awaitable.cancel(); + } else { + Pair.canceller(); + } + }; + (((idx++ == winner) ? void() : cancelSync(Pairs)), ...); + } else { + // At least once cancellation is async. The fold expression's `co_await` must resolve + // for all cancellations, so sync cancellations are projected with a dummy awaitable. + size_t idx = 0; + auto cancelAsync = [](auto& Pair) -> decltype(auto) { + using Canceller_t = std::remove_reference_t; + if constexpr (std::is_same_v) { + if constexpr (tmc::traits::is_awaitable) { + return Pair.awaitable.cancel(); + } else { + Pair.awaitable.cancel(); + return tmc::detail::cancel_noop{}; + } + } else { + if constexpr (tmc::traits::is_awaitable) { + return Pair.canceller(); + } else { + Pair.canceller(); + return tmc::detail::cancel_noop{}; + } + } + }; + (((idx++ == winner) ? void() : (void)(co_await cancelAsync(Pairs))), ...); + } + + // Drain the remaining (now-cancelled) awaitables. Their results are + // discarded, but they must all complete before `each` is destroyed. + for (size_t i = co_await each; i != each.end(); i = co_await each) { + // discard + } + + co_return std::move(*result); +} +} // namespace tmc diff --git a/include/tmc/spawn_tuple.hpp b/include/tmc/spawn_tuple.hpp index b7019c40..7d8bd120 100644 --- a/include/tmc/spawn_tuple.hpp +++ b/include/tmc/spawn_tuple.hpp @@ -26,8 +26,7 @@ namespace tmc { namespace detail { // Replace void with std::monostate (void is not a valid tuple element type) template -using void_to_monostate = - std::conditional_t, std::monostate, T>; +using void_to_monostate = std::conditional_t, std::monostate, T>; // Get the last type of a parameter pack // In C++26 you can use pack indexing instead: T...[sizeof...(T) - 1] @@ -44,9 +43,7 @@ template using last_type_t = typename last_type::type; // Create 2 instantiations of the Variadic, one where all of the types // satisfy the predicate, and one where none of the types satisfy the predicate. -template < - template class Predicate, template class Variadic, - class...> +template