1  
//
1  
//
2  
// Copyright (c) 2026 Michael Vandeberg
2  
// Copyright (c) 2026 Michael Vandeberg
3  
//
3  
//
4  
// Distributed under the Boost Software License, Version 1.0. (See accompanying
4  
// Distributed under the Boost Software License, Version 1.0. (See accompanying
5  
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
5  
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6  
//
6  
//
7  
// Official repository: https://github.com/cppalliance/capy
7  
// Official repository: https://github.com/cppalliance/capy
8  
//
8  
//
9  

9  

10  
#ifndef BOOST_CAPY_WHEN_ANY_HPP
10  
#ifndef BOOST_CAPY_WHEN_ANY_HPP
11  
#define BOOST_CAPY_WHEN_ANY_HPP
11  
#define BOOST_CAPY_WHEN_ANY_HPP
12  

12  

13  
#include <boost/capy/detail/config.hpp>
13  
#include <boost/capy/detail/config.hpp>
14  
#include <boost/capy/concept/executor.hpp>
14  
#include <boost/capy/concept/executor.hpp>
15  
#include <boost/capy/concept/io_awaitable.hpp>
15  
#include <boost/capy/concept/io_awaitable.hpp>
16  
#include <coroutine>
16  
#include <coroutine>
17  
#include <boost/capy/ex/executor_ref.hpp>
17  
#include <boost/capy/ex/executor_ref.hpp>
18  
#include <boost/capy/ex/frame_allocator.hpp>
18  
#include <boost/capy/ex/frame_allocator.hpp>
19  
#include <boost/capy/ex/io_env.hpp>
19  
#include <boost/capy/ex/io_env.hpp>
20  
#include <boost/capy/task.hpp>
20  
#include <boost/capy/task.hpp>
21  

21  

22  
#include <array>
22  
#include <array>
23  
#include <atomic>
23  
#include <atomic>
24  
#include <exception>
24  
#include <exception>
25  
#include <optional>
25  
#include <optional>
26  
#include <ranges>
26  
#include <ranges>
27  
#include <stdexcept>
27  
#include <stdexcept>
28  
#include <stop_token>
28  
#include <stop_token>
29  
#include <tuple>
29  
#include <tuple>
30  
#include <type_traits>
30  
#include <type_traits>
31  
#include <utility>
31  
#include <utility>
32  
#include <variant>
32  
#include <variant>
33  
#include <vector>
33  
#include <vector>
34  

34  

35  
/*
35  
/*
36  
   when_any - Race multiple tasks, return first completion
36  
   when_any - Race multiple tasks, return first completion
37  
   ========================================================
37  
   ========================================================
38  

38  

39  
   OVERVIEW:
39  
   OVERVIEW:
40  
   ---------
40  
   ---------
41  
   when_any launches N tasks concurrently and completes when the FIRST task
41  
   when_any launches N tasks concurrently and completes when the FIRST task
42  
   finishes (success or failure). It then requests stop for all siblings and
42  
   finishes (success or failure). It then requests stop for all siblings and
43  
   waits for them to acknowledge before returning.
43  
   waits for them to acknowledge before returning.
44  

44  

45  
   ARCHITECTURE:
45  
   ARCHITECTURE:
46  
   -------------
46  
   -------------
47  
   The design mirrors when_all but with inverted completion semantics:
47  
   The design mirrors when_all but with inverted completion semantics:
48  

48  

49  
     when_all:  complete when remaining_count reaches 0 (all done)
49  
     when_all:  complete when remaining_count reaches 0 (all done)
50  
     when_any:  complete when has_winner becomes true (first done)
50  
     when_any:  complete when has_winner becomes true (first done)
51  
                BUT still wait for remaining_count to reach 0 for cleanup
51  
                BUT still wait for remaining_count to reach 0 for cleanup
52  

52  

53  
   Key components:
53  
   Key components:
54  
     - when_any_state:    Shared state tracking winner and completion
54  
     - when_any_state:    Shared state tracking winner and completion
55  
     - when_any_runner:   Wrapper coroutine for each child task
55  
     - when_any_runner:   Wrapper coroutine for each child task
56  
     - when_any_launcher: Awaitable that starts all runners concurrently
56  
     - when_any_launcher: Awaitable that starts all runners concurrently
57  

57  

58  
   CRITICAL INVARIANTS:
58  
   CRITICAL INVARIANTS:
59  
   --------------------
59  
   --------------------
60  
   1. Exactly one task becomes the winner (via atomic compare_exchange)
60  
   1. Exactly one task becomes the winner (via atomic compare_exchange)
61  
   2. All tasks must complete before parent resumes (cleanup safety)
61  
   2. All tasks must complete before parent resumes (cleanup safety)
62  
   3. Stop is requested immediately when winner is determined
62  
   3. Stop is requested immediately when winner is determined
63  
   4. Only the winner's result/exception is stored
63  
   4. Only the winner's result/exception is stored
64  

64  

65 -
   POSITIONAL VARIANT:
65 +
   TYPE DEDUPLICATION:
66  
   -------------------
66  
   -------------------
67 -
   The variadic overload returns a std::variant with one alternative per
67 +
   std::variant requires unique alternative types. Since when_any can race
68 -
   input task, preserving positional correspondence. Use .index() on
68 +
   tasks with identical return types (e.g., three task<int>), we must
69 -
   the variant to identify which task won.
69 +
   deduplicate types before constructing the variant.
70  

70  

71  
   Example: when_any(task<int>, task<string>, task<int>)
71  
   Example: when_any(task<int>, task<string>, task<int>)
72  
     - Raw types after void->monostate: int, string, int
72  
     - Raw types after void->monostate: int, string, int
73 -
     - Result variant: std::variant<int, string, int>
73 +
     - Deduplicated variant: std::variant<int, string>
74 -
     - variant.index() tells you which task won (0, 1, or 2)
74 +
     - Return: pair<size_t, variant<int, string>>
 
75 +

 
76 +
   The winner_index tells you which task won (0, 1, or 2), while the variant
 
77 +
   holds the result. Use the index to determine how to interpret the variant.
75  

78  

76  
   VOID HANDLING:
79  
   VOID HANDLING:
77  
   --------------
80  
   --------------
78 -
   void tasks contribute std::monostate to the variant.
81 +
   void tasks contribute std::monostate to the variant (then deduplicated).
79 -
   All-void tasks result in: variant<monostate, monostate, monostate>
82 +
   All-void tasks result in: pair<size_t, variant<monostate>>
80  

83  

81  
   MEMORY MODEL:
84  
   MEMORY MODEL:
82  
   -------------
85  
   -------------
83  
   Synchronization chain from winner's write to parent's read:
86  
   Synchronization chain from winner's write to parent's read:
84  

87  

85  
   1. Winner thread writes result_/winner_exception_ (non-atomic)
88  
   1. Winner thread writes result_/winner_exception_ (non-atomic)
86  
   2. Winner thread calls signal_completion() → fetch_sub(acq_rel) on remaining_count_
89  
   2. Winner thread calls signal_completion() → fetch_sub(acq_rel) on remaining_count_
87  
   3. Last task thread (may be winner or non-winner) calls signal_completion()
90  
   3. Last task thread (may be winner or non-winner) calls signal_completion()
88  
      → fetch_sub(acq_rel) on remaining_count_, observing count becomes 0
91  
      → fetch_sub(acq_rel) on remaining_count_, observing count becomes 0
89  
   4. Last task returns caller_ex_.dispatch(continuation_) via symmetric transfer
92  
   4. Last task returns caller_ex_.dispatch(continuation_) via symmetric transfer
90  
   5. Parent coroutine resumes and reads result_/winner_exception_
93  
   5. Parent coroutine resumes and reads result_/winner_exception_
91  

94  

92  
   Synchronization analysis:
95  
   Synchronization analysis:
93  
   - All fetch_sub operations on remaining_count_ form a release sequence
96  
   - All fetch_sub operations on remaining_count_ form a release sequence
94  
   - Winner's fetch_sub releases; subsequent fetch_sub operations participate
97  
   - Winner's fetch_sub releases; subsequent fetch_sub operations participate
95  
     in the modification order of remaining_count_
98  
     in the modification order of remaining_count_
96  
   - Last task's fetch_sub(acq_rel) synchronizes-with prior releases in the
99  
   - Last task's fetch_sub(acq_rel) synchronizes-with prior releases in the
97  
     modification order, establishing happens-before from winner's writes
100  
     modification order, establishing happens-before from winner's writes
98  
   - Executor dispatch() is expected to provide queue-based synchronization
101  
   - Executor dispatch() is expected to provide queue-based synchronization
99  
     (release-on-post, acquire-on-execute) completing the chain to parent
102  
     (release-on-post, acquire-on-execute) completing the chain to parent
100  
   - Even inline executors work (same thread = sequenced-before)
103  
   - Even inline executors work (same thread = sequenced-before)
101  

104  

102  
   Alternative considered: Adding winner_ready_ atomic (set with release after
105  
   Alternative considered: Adding winner_ready_ atomic (set with release after
103  
   storing winner data, acquired before reading) would make synchronization
106  
   storing winner data, acquired before reading) would make synchronization
104  
   self-contained and not rely on executor implementation details. Current
107  
   self-contained and not rely on executor implementation details. Current
105  
   approach is correct but requires careful reasoning about release sequences
108  
   approach is correct but requires careful reasoning about release sequences
106  
   and executor behavior.
109  
   and executor behavior.
107  

110  

108  
   EXCEPTION SEMANTICS:
111  
   EXCEPTION SEMANTICS:
109  
   --------------------
112  
   --------------------
110  
   Unlike when_all (which captures first exception, discards others), when_any
113  
   Unlike when_all (which captures first exception, discards others), when_any
111  
   treats exceptions as valid completions. If the winning task threw, that
114  
   treats exceptions as valid completions. If the winning task threw, that
112  
   exception is rethrown. Exceptions from non-winners are silently discarded.
115  
   exception is rethrown. Exceptions from non-winners are silently discarded.
113  
*/
116  
*/
114  

117  

115  
namespace boost {
118  
namespace boost {
116  
namespace capy {
119  
namespace capy {
117  

120  

118  
namespace detail {
121  
namespace detail {
119  

122  

120  
/** Convert void to monostate for variant storage.
123  
/** Convert void to monostate for variant storage.
121  

124  

122  
    std::variant<void, ...> is ill-formed, so void tasks contribute
125  
    std::variant<void, ...> is ill-formed, so void tasks contribute
123  
    std::monostate to the result variant instead. Non-void types
126  
    std::monostate to the result variant instead. Non-void types
124  
    pass through unchanged.
127  
    pass through unchanged.
125  

128  

126  
    @tparam T The type to potentially convert (void becomes monostate).
129  
    @tparam T The type to potentially convert (void becomes monostate).
127  
*/
130  
*/
128  
template<typename T>
131  
template<typename T>
129  
using void_to_monostate_t = std::conditional_t<std::is_void_v<T>, std::monostate, T>;
132  
using void_to_monostate_t = std::conditional_t<std::is_void_v<T>, std::monostate, T>;
130  

133  

131 -
// Result variant: one alternative per task, preserving positional
134 +
// Type deduplication: std::variant requires unique alternative types.
132 -
// correspondence. Use .index() to identify which task won.
135 +
// Fold left over the type list, appending each type only if not already present.
133 -
// void results become monostate.
136 +
template<typename Variant, typename T>
 
137 +
struct variant_append_if_unique;
 
138 +

 
139 +
template<typename... Vs, typename T>
 
140 +
struct variant_append_if_unique<std::variant<Vs...>, T>
 
141 +
{
 
142 +
    using type = std::conditional_t<
 
143 +
        (std::is_same_v<T, Vs> || ...),
 
144 +
        std::variant<Vs...>,
 
145 +
        std::variant<Vs..., T>>;
 
146 +
};
 
147 +

 
148 +
template<typename Accumulated, typename... Remaining>
 
149 +
struct deduplicate_impl;
 
150 +

 
151 +
template<typename Accumulated>
 
152 +
struct deduplicate_impl<Accumulated>
 
153 +
{
 
154 +
    using type = Accumulated;
 
155 +
};
 
156 +

 
157 +
template<typename Accumulated, typename T, typename... Rest>
 
158 +
struct deduplicate_impl<Accumulated, T, Rest...>
 
159 +
{
 
160 +
    using next = typename variant_append_if_unique<Accumulated, T>::type;
 
161 +
    using type = typename deduplicate_impl<next, Rest...>::type;
 
162 +
};
 
163 +

 
164 +
// Deduplicated variant; void types become monostate before deduplication
134  
template<typename T0, typename... Ts>
165  
template<typename T0, typename... Ts>
135 -
using when_any_variant_t = std::variant<void_to_monostate_t<T0>, void_to_monostate_t<Ts>...>;
166 +
using unique_variant_t = typename deduplicate_impl<
 
167 +
    std::variant<void_to_monostate_t<T0>>,
 
168 +
    void_to_monostate_t<Ts>...>::type;
 
169 +

 
170 +
// Result: (winner_index, deduplicated_variant). Use index to disambiguate
 
171 +
// when multiple tasks share the same return type.
 
172 +
template<typename T0, typename... Ts>
 
173 +
using when_any_result_t = std::pair<std::size_t, unique_variant_t<T0, Ts...>>;
136  

174  

137  
/** Core shared state for when_any operations.
175  
/** Core shared state for when_any operations.
138  

176  

139  
    Contains all members and methods common to both heterogeneous (variadic)
177  
    Contains all members and methods common to both heterogeneous (variadic)
140  
    and homogeneous (range) when_any implementations. State classes embed
178  
    and homogeneous (range) when_any implementations. State classes embed
141  
    this via composition to avoid CRTP destructor ordering issues.
179  
    this via composition to avoid CRTP destructor ordering issues.
142  

180  

143  
    @par Thread Safety
181  
    @par Thread Safety
144  
    Atomic operations protect winner selection and completion count.
182  
    Atomic operations protect winner selection and completion count.
145  
*/
183  
*/
146  
struct when_any_core
184  
struct when_any_core
147  
{
185  
{
148  
    std::atomic<std::size_t> remaining_count_;
186  
    std::atomic<std::size_t> remaining_count_;
149  
    std::size_t winner_index_{0};
187  
    std::size_t winner_index_{0};
150  
    std::exception_ptr winner_exception_;
188  
    std::exception_ptr winner_exception_;
151  
    std::stop_source stop_source_;
189  
    std::stop_source stop_source_;
152  

190  

153  
    // Bridges parent's stop token to our stop_source
191  
    // Bridges parent's stop token to our stop_source
154  
    struct stop_callback_fn
192  
    struct stop_callback_fn
155  
    {
193  
    {
156  
        std::stop_source* source_;
194  
        std::stop_source* source_;
157  
        void operator()() const noexcept { source_->request_stop(); }
195  
        void operator()() const noexcept { source_->request_stop(); }
158  
    };
196  
    };
159  
    using stop_callback_t = std::stop_callback<stop_callback_fn>;
197  
    using stop_callback_t = std::stop_callback<stop_callback_fn>;
160  
    std::optional<stop_callback_t> parent_stop_callback_;
198  
    std::optional<stop_callback_t> parent_stop_callback_;
161  

199  

162  
    std::coroutine_handle<> continuation_;
200  
    std::coroutine_handle<> continuation_;
163  
    io_env const* caller_env_ = nullptr;
201  
    io_env const* caller_env_ = nullptr;
164  

202  

165  
    // Placed last to avoid padding (1-byte atomic followed by 8-byte aligned members)
203  
    // Placed last to avoid padding (1-byte atomic followed by 8-byte aligned members)
166  
    std::atomic<bool> has_winner_{false};
204  
    std::atomic<bool> has_winner_{false};
167  

205  

168  
    explicit when_any_core(std::size_t count) noexcept
206  
    explicit when_any_core(std::size_t count) noexcept
169  
        : remaining_count_(count)
207  
        : remaining_count_(count)
170  
    {
208  
    {
171  
    }
209  
    }
172  

210  

173  
    /** Atomically claim winner status; exactly one task succeeds. */
211  
    /** Atomically claim winner status; exactly one task succeeds. */
174  
    bool try_win(std::size_t index) noexcept
212  
    bool try_win(std::size_t index) noexcept
175  
    {
213  
    {
176  
        bool expected = false;
214  
        bool expected = false;
177  
        if(has_winner_.compare_exchange_strong(
215  
        if(has_winner_.compare_exchange_strong(
178  
            expected, true, std::memory_order_acq_rel))
216  
            expected, true, std::memory_order_acq_rel))
179  
        {
217  
        {
180  
            winner_index_ = index;
218  
            winner_index_ = index;
181  
            stop_source_.request_stop();
219  
            stop_source_.request_stop();
182  
            return true;
220  
            return true;
183  
        }
221  
        }
184  
        return false;
222  
        return false;
185  
    }
223  
    }
186  

224  

187  
    /** @pre try_win() returned true. */
225  
    /** @pre try_win() returned true. */
188  
    void set_winner_exception(std::exception_ptr ep) noexcept
226  
    void set_winner_exception(std::exception_ptr ep) noexcept
189  
    {
227  
    {
190  
        winner_exception_ = ep;
228  
        winner_exception_ = ep;
191  
    }
229  
    }
192  

230  

193  
    // Runners signal completion directly via final_suspend; no member function needed.
231  
    // Runners signal completion directly via final_suspend; no member function needed.
194  
};
232  
};
195  

233  

196  
/** Shared state for heterogeneous when_any operation.
234  
/** Shared state for heterogeneous when_any operation.
197  

235  

198  
    Coordinates winner selection, result storage, and completion tracking
236  
    Coordinates winner selection, result storage, and completion tracking
199  
    for all child tasks in a when_any operation. Uses composition with
237  
    for all child tasks in a when_any operation. Uses composition with
200  
    when_any_core for shared functionality.
238  
    when_any_core for shared functionality.
201  

239  

202  
    @par Lifetime
240  
    @par Lifetime
203  
    Allocated on the parent coroutine's frame, outlives all runners.
241  
    Allocated on the parent coroutine's frame, outlives all runners.
204  

242  

205  
    @tparam T0 First task's result type.
243  
    @tparam T0 First task's result type.
206  
    @tparam Ts Remaining tasks' result types.
244  
    @tparam Ts Remaining tasks' result types.
207  
*/
245  
*/
208  
template<typename T0, typename... Ts>
246  
template<typename T0, typename... Ts>
209  
struct when_any_state
247  
struct when_any_state
210  
{
248  
{
211  
    static constexpr std::size_t task_count = 1 + sizeof...(Ts);
249  
    static constexpr std::size_t task_count = 1 + sizeof...(Ts);
212 -
    using variant_type = when_any_variant_t<T0, Ts...>;
250 +
    using variant_type = unique_variant_t<T0, Ts...>;
213  

251  

214  
    when_any_core core_;
252  
    when_any_core core_;
215  
    std::optional<variant_type> result_;
253  
    std::optional<variant_type> result_;
216  
    std::array<std::coroutine_handle<>, task_count> runner_handles_{};
254  
    std::array<std::coroutine_handle<>, task_count> runner_handles_{};
217  

255  

218  
    when_any_state()
256  
    when_any_state()
219  
        : core_(task_count)
257  
        : core_(task_count)
220  
    {
258  
    {
221  
    }
259  
    }
222  

260  

223  
    // Runners self-destruct in final_suspend. No destruction needed here.
261  
    // Runners self-destruct in final_suspend. No destruction needed here.
224  

262  

225  
    /** @pre core_.try_win() returned true.
263  
    /** @pre core_.try_win() returned true.
226 -
        @note Uses in_place_index (not type) for positional variant access.
264 +
        @note Uses in_place_type (not index) because variant is deduplicated.
227  
    */
265  
    */
228 -
    template<std::size_t I, typename T>
266 +
    template<typename T>
229  
    void set_winner_result(T value)
267  
    void set_winner_result(T value)
230  
        noexcept(std::is_nothrow_move_constructible_v<T>)
268  
        noexcept(std::is_nothrow_move_constructible_v<T>)
231  
    {
269  
    {
232 -
        result_.emplace(std::in_place_index<I>, std::move(value));
270 +
        result_.emplace(std::in_place_type<T>, std::move(value));
233  
    }
271  
    }
234  

272  

235 -
    template<std::size_t I>
 
236  
    /** @pre core_.try_win() returned true. */
273  
    /** @pre core_.try_win() returned true. */
237  
    void set_winner_void() noexcept
274  
    void set_winner_void() noexcept
238  
    {
275  
    {
239 -
        result_.emplace(std::in_place_index<I>, std::monostate{});
276 +
        result_.emplace(std::in_place_type<std::monostate>, std::monostate{});
240  
    }
277  
    }
241  
};
278  
};
242  

279  

243  
/** Wrapper coroutine that runs a single child task for when_any.
280  
/** Wrapper coroutine that runs a single child task for when_any.
244  

281  

245  
    Propagates executor/stop_token to the child, attempts to claim winner
282  
    Propagates executor/stop_token to the child, attempts to claim winner
246  
    status on completion, and signals completion for cleanup coordination.
283  
    status on completion, and signals completion for cleanup coordination.
247  

284  

248  
    @tparam StateType The state type (when_any_state or when_any_homogeneous_state).
285  
    @tparam StateType The state type (when_any_state or when_any_homogeneous_state).
249  
*/
286  
*/
250  
template<typename StateType>
287  
template<typename StateType>
251  
struct when_any_runner
288  
struct when_any_runner
252  
{
289  
{
253  
    struct promise_type // : frame_allocating_base  // DISABLED FOR TESTING
290  
    struct promise_type // : frame_allocating_base  // DISABLED FOR TESTING
254  
    {
291  
    {
255  
        StateType* state_ = nullptr;
292  
        StateType* state_ = nullptr;
256  
        std::size_t index_ = 0;
293  
        std::size_t index_ = 0;
257  
        io_env env_;
294  
        io_env env_;
258  

295  

259  
        when_any_runner get_return_object() noexcept
296  
        when_any_runner get_return_object() noexcept
260  
        {
297  
        {
261  
            return when_any_runner(std::coroutine_handle<promise_type>::from_promise(*this));
298  
            return when_any_runner(std::coroutine_handle<promise_type>::from_promise(*this));
262  
        }
299  
        }
263  

300  

264  
        // Starts suspended; launcher sets up state/ex/token then resumes
301  
        // Starts suspended; launcher sets up state/ex/token then resumes
265  
        std::suspend_always initial_suspend() noexcept
302  
        std::suspend_always initial_suspend() noexcept
266  
        {
303  
        {
267  
            return {};
304  
            return {};
268  
        }
305  
        }
269  

306  

270  
        auto final_suspend() noexcept
307  
        auto final_suspend() noexcept
271  
        {
308  
        {
272  
            struct awaiter
309  
            struct awaiter
273  
            {
310  
            {
274  
                promise_type* p_;
311  
                promise_type* p_;
275  
                bool await_ready() const noexcept { return false; }
312  
                bool await_ready() const noexcept { return false; }
276 -
                std::coroutine_handle<> await_suspend(std::coroutine_handle<> h) noexcept
313 +
                auto await_suspend(std::coroutine_handle<> h) noexcept
277  
                {
314  
                {
278  
                    // Extract everything needed before self-destruction.
315  
                    // Extract everything needed before self-destruction.
279  
                    auto& core = p_->state_->core_;
316  
                    auto& core = p_->state_->core_;
280  
                    auto* counter = &core.remaining_count_;
317  
                    auto* counter = &core.remaining_count_;
281  
                    auto* caller_env = core.caller_env_;
318  
                    auto* caller_env = core.caller_env_;
282  
                    auto cont = core.continuation_;
319  
                    auto cont = core.continuation_;
283  

320  

284  
                    h.destroy();
321  
                    h.destroy();
285  

322  

286  
                    // If last runner, dispatch parent for symmetric transfer.
323  
                    // If last runner, dispatch parent for symmetric transfer.
287  
                    auto remaining = counter->fetch_sub(1, std::memory_order_acq_rel);
324  
                    auto remaining = counter->fetch_sub(1, std::memory_order_acq_rel);
288  
                    if(remaining == 1)
325  
                    if(remaining == 1)
289 -
                        return caller_env->executor.dispatch(cont);
326 +
                        return detail::symmetric_transfer(caller_env->executor.dispatch(cont));
290 -
                    return std::noop_coroutine();
327 +
                    return detail::symmetric_transfer(std::noop_coroutine());
291  
                }
328  
                }
292  
                void await_resume() const noexcept {}
329  
                void await_resume() const noexcept {}
293  
            };
330  
            };
294  
            return awaiter{this};
331  
            return awaiter{this};
295  
        }
332  
        }
296  

333  

297  
        void return_void() noexcept {}
334  
        void return_void() noexcept {}
298  

335  

299  
        // Exceptions are valid completions in when_any (unlike when_all)
336  
        // Exceptions are valid completions in when_any (unlike when_all)
300  
        void unhandled_exception()
337  
        void unhandled_exception()
301  
        {
338  
        {
302  
            if(state_->core_.try_win(index_))
339  
            if(state_->core_.try_win(index_))
303  
                state_->core_.set_winner_exception(std::current_exception());
340  
                state_->core_.set_winner_exception(std::current_exception());
304  
        }
341  
        }
305  

342  

306  
        /** Injects executor and stop token into child awaitables. */
343  
        /** Injects executor and stop token into child awaitables. */
307  
        template<class Awaitable>
344  
        template<class Awaitable>
308  
        struct transform_awaiter
345  
        struct transform_awaiter
309  
        {
346  
        {
310  
            std::decay_t<Awaitable> a_;
347  
            std::decay_t<Awaitable> a_;
311  
            promise_type* p_;
348  
            promise_type* p_;
312  

349  

313  
            bool await_ready() { return a_.await_ready(); }
350  
            bool await_ready() { return a_.await_ready(); }
314  
            auto await_resume() { return a_.await_resume(); }
351  
            auto await_resume() { return a_.await_resume(); }
315  

352  

316  
            template<class Promise>
353  
            template<class Promise>
317  
            auto await_suspend(std::coroutine_handle<Promise> h)
354  
            auto await_suspend(std::coroutine_handle<Promise> h)
318 -
#ifdef _MSC_VER
 
319  
            {
355  
            {
320  
                using R = decltype(a_.await_suspend(h, &p_->env_));
356  
                using R = decltype(a_.await_suspend(h, &p_->env_));
321  
                if constexpr (std::is_same_v<R, std::coroutine_handle<>>)
357  
                if constexpr (std::is_same_v<R, std::coroutine_handle<>>)
322 -
                    a_.await_suspend(h, &p_->env_).resume();
358 +
                    return detail::symmetric_transfer(a_.await_suspend(h, &p_->env_));
323  
                else
359  
                else
324 -
#else
 
325 -
                return a_.await_suspend(h, &p_->env_);
 
326 -
#endif
 
327  
                    return a_.await_suspend(h, &p_->env_);
360  
                    return a_.await_suspend(h, &p_->env_);
328  
            }
361  
            }
329  
        };
362  
        };
330  

363  

331  
        template<class Awaitable>
364  
        template<class Awaitable>
332  
        auto await_transform(Awaitable&& a)
365  
        auto await_transform(Awaitable&& a)
333  
        {
366  
        {
334  
            using A = std::decay_t<Awaitable>;
367  
            using A = std::decay_t<Awaitable>;
335  
            if constexpr (IoAwaitable<A>)
368  
            if constexpr (IoAwaitable<A>)
336  
            {
369  
            {
337  
                return transform_awaiter<Awaitable>{
370  
                return transform_awaiter<Awaitable>{
338  
                    std::forward<Awaitable>(a), this};
371  
                    std::forward<Awaitable>(a), this};
339  
            }
372  
            }
340  
            else
373  
            else
341  
            {
374  
            {
342  
                static_assert(sizeof(A) == 0, "requires IoAwaitable");
375  
                static_assert(sizeof(A) == 0, "requires IoAwaitable");
343  
            }
376  
            }
344  
        }
377  
        }
345  
    };
378  
    };
346  

379  

347  
    std::coroutine_handle<promise_type> h_;
380  
    std::coroutine_handle<promise_type> h_;
348  

381  

349  
    explicit when_any_runner(std::coroutine_handle<promise_type> h) noexcept
382  
    explicit when_any_runner(std::coroutine_handle<promise_type> h) noexcept
350  
        : h_(h)
383  
        : h_(h)
351  
    {
384  
    {
352  
    }
385  
    }
353  

386  

354  
    // Enable move for all clang versions - some versions need it
387  
    // Enable move for all clang versions - some versions need it
355  
    when_any_runner(when_any_runner&& other) noexcept : h_(std::exchange(other.h_, nullptr)) {}
388  
    when_any_runner(when_any_runner&& other) noexcept : h_(std::exchange(other.h_, nullptr)) {}
356  

389  

357  
    // Non-copyable
390  
    // Non-copyable
358  
    when_any_runner(when_any_runner const&) = delete;
391  
    when_any_runner(when_any_runner const&) = delete;
359  
    when_any_runner& operator=(when_any_runner const&) = delete;
392  
    when_any_runner& operator=(when_any_runner const&) = delete;
360  
    when_any_runner& operator=(when_any_runner&&) = delete;
393  
    when_any_runner& operator=(when_any_runner&&) = delete;
361  

394  

362  
    auto release() noexcept
395  
    auto release() noexcept
363  
    {
396  
    {
364  
        return std::exchange(h_, nullptr);
397  
        return std::exchange(h_, nullptr);
365  
    }
398  
    }
366  
};
399  
};
367  

400  

368 -
/** Indexed overload for heterogeneous when_any (compile-time index).
401 +
/** Wraps a child awaitable, attempts to claim winner on completion.
369 -

 
370 -
    Uses compile-time index I for variant construction via in_place_index.
 
371 -
    Called from when_any_launcher::launch_one<I>().
 
372 -
*/
 
373 -
template<std::size_t I, IoAwaitable Awaitable, typename StateType>
 
374 -
when_any_runner<StateType>
 
375 -
make_when_any_runner(Awaitable inner, StateType* state)
 
376 -
{
 
377 -
    using T = awaitable_result_t<Awaitable>;
 
378 -
    if constexpr (std::is_void_v<T>)
 
379 -
    {
 
380 -
        co_await std::move(inner);
 
381 -
        if(state->core_.try_win(I))
 
382 -
            state->template set_winner_void<I>();
 
383 -
    }
 
384 -
    else
 
385 -
    {
 
386 -
        auto result = co_await std::move(inner);
 
387 -
        if(state->core_.try_win(I))
 
388 -
        {
 
389 -
            try
 
390 -
            {
 
391 -
                state->template set_winner_result<I>(std::move(result));
 
392 -
            }
 
393 -
            catch(...)
 
394 -
            {
 
395 -
                state->core_.set_winner_exception(std::current_exception());
 
396 -
            }
 
397 -
        }
 
398 -
    }
 
399 -
}
 
400 -

 
401 -
/** Runtime-index overload for homogeneous when_any (range path).
 
402  

402  

403  
    Uses requires-expressions to detect state capabilities:
403  
    Uses requires-expressions to detect state capabilities:
404  
    - set_winner_void(): for heterogeneous void tasks (stores monostate)
404  
    - set_winner_void(): for heterogeneous void tasks (stores monostate)
405  
    - set_winner_result(): for non-void tasks
405  
    - set_winner_result(): for non-void tasks
406  
    - Neither: for homogeneous void tasks (no result storage)
406  
    - Neither: for homogeneous void tasks (no result storage)
407  
*/
407  
*/
408  
template<IoAwaitable Awaitable, typename StateType>
408  
template<IoAwaitable Awaitable, typename StateType>
409  
when_any_runner<StateType>
409  
when_any_runner<StateType>
410  
make_when_any_runner(Awaitable inner, StateType* state, std::size_t index)
410  
make_when_any_runner(Awaitable inner, StateType* state, std::size_t index)
411  
{
411  
{
412  
    using T = awaitable_result_t<Awaitable>;
412  
    using T = awaitable_result_t<Awaitable>;
413  
    if constexpr (std::is_void_v<T>)
413  
    if constexpr (std::is_void_v<T>)
414  
    {
414  
    {
415  
        co_await std::move(inner);
415  
        co_await std::move(inner);
416  
        if(state->core_.try_win(index))
416  
        if(state->core_.try_win(index))
417  
        {
417  
        {
 
418 +
            // Heterogeneous void tasks store monostate in the variant
418  
            if constexpr (requires { state->set_winner_void(); })
419  
            if constexpr (requires { state->set_winner_void(); })
419  
                state->set_winner_void();
420  
                state->set_winner_void();
 
421 +
            // Homogeneous void tasks have no result to store
420  
        }
422  
        }
421  
    }
423  
    }
422  
    else
424  
    else
423  
    {
425  
    {
424  
        auto result = co_await std::move(inner);
426  
        auto result = co_await std::move(inner);
425  
        if(state->core_.try_win(index))
427  
        if(state->core_.try_win(index))
426  
        {
428  
        {
 
429 +
            // Defensive: move should not throw (already moved once), but we
 
430 +
            // catch just in case since an uncaught exception would be devastating.
427  
            try
431  
            try
428  
            {
432  
            {
429  
                state->set_winner_result(std::move(result));
433  
                state->set_winner_result(std::move(result));
430  
            }
434  
            }
431  
            catch(...)
435  
            catch(...)
432  
            {
436  
            {
433  
                state->core_.set_winner_exception(std::current_exception());
437  
                state->core_.set_winner_exception(std::current_exception());
434  
            }
438  
            }
435  
        }
439  
        }
436  
    }
440  
    }
437  
}
441  
}
438  

442  

439  
/** Launches all runners concurrently; see await_suspend for lifetime concerns. */
443  
/** Launches all runners concurrently; see await_suspend for lifetime concerns. */
440  
template<IoAwaitable... Awaitables>
444  
template<IoAwaitable... Awaitables>
441  
class when_any_launcher
445  
class when_any_launcher
442  
{
446  
{
443  
    using state_type = when_any_state<awaitable_result_t<Awaitables>...>;
447  
    using state_type = when_any_state<awaitable_result_t<Awaitables>...>;
444  

448  

445  
    std::tuple<Awaitables...>* tasks_;
449  
    std::tuple<Awaitables...>* tasks_;
446  
    state_type* state_;
450  
    state_type* state_;
447  

451  

448  
public:
452  
public:
449  
    when_any_launcher(
453  
    when_any_launcher(
450  
        std::tuple<Awaitables...>* tasks,
454  
        std::tuple<Awaitables...>* tasks,
451  
        state_type* state)
455  
        state_type* state)
452  
        : tasks_(tasks)
456  
        : tasks_(tasks)
453  
        , state_(state)
457  
        , state_(state)
454  
    {
458  
    {
455  
    }
459  
    }
456  

460  

457  
    bool await_ready() const noexcept
461  
    bool await_ready() const noexcept
458  
    {
462  
    {
459  
        return sizeof...(Awaitables) == 0;
463  
        return sizeof...(Awaitables) == 0;
460  
    }
464  
    }
461  

465  

462  
    /** CRITICAL: If the last task finishes synchronously, parent resumes and
466  
    /** CRITICAL: If the last task finishes synchronously, parent resumes and
463  
        destroys this object before await_suspend returns. Must not reference
467  
        destroys this object before await_suspend returns. Must not reference
464  
        `this` after the final launch_one call.
468  
        `this` after the final launch_one call.
465  
    */
469  
    */
466  
    std::coroutine_handle<> await_suspend(std::coroutine_handle<> continuation, io_env const* caller_env)
470  
    std::coroutine_handle<> await_suspend(std::coroutine_handle<> continuation, io_env const* caller_env)
467  
    {
471  
    {
468  
        state_->core_.continuation_ = continuation;
472  
        state_->core_.continuation_ = continuation;
469  
        state_->core_.caller_env_ = caller_env;
473  
        state_->core_.caller_env_ = caller_env;
470  

474  

471  
        if(caller_env->stop_token.stop_possible())
475  
        if(caller_env->stop_token.stop_possible())
472  
        {
476  
        {
473  
            state_->core_.parent_stop_callback_.emplace(
477  
            state_->core_.parent_stop_callback_.emplace(
474  
                caller_env->stop_token,
478  
                caller_env->stop_token,
475  
                when_any_core::stop_callback_fn{&state_->core_.stop_source_});
479  
                when_any_core::stop_callback_fn{&state_->core_.stop_source_});
476  

480  

477  
            if(caller_env->stop_token.stop_requested())
481  
            if(caller_env->stop_token.stop_requested())
478  
                state_->core_.stop_source_.request_stop();
482  
                state_->core_.stop_source_.request_stop();
479  
        }
483  
        }
480  

484  

481  
        auto token = state_->core_.stop_source_.get_token();
485  
        auto token = state_->core_.stop_source_.get_token();
482  
        [&]<std::size_t... Is>(std::index_sequence<Is...>) {
486  
        [&]<std::size_t... Is>(std::index_sequence<Is...>) {
483  
            (..., launch_one<Is>(caller_env->executor, token));
487  
            (..., launch_one<Is>(caller_env->executor, token));
484  
        }(std::index_sequence_for<Awaitables...>{});
488  
        }(std::index_sequence_for<Awaitables...>{});
485  

489  

486  
        return std::noop_coroutine();
490  
        return std::noop_coroutine();
487  
    }
491  
    }
488  

492  

489  
    void await_resume() const noexcept
493  
    void await_resume() const noexcept
490  
    {
494  
    {
491  
    }
495  
    }
492  

496  

493  
private:
497  
private:
494  
    /** @pre Ex::dispatch() and std::coroutine_handle<>::resume() must not throw (handle may leak). */
498  
    /** @pre Ex::dispatch() and std::coroutine_handle<>::resume() must not throw (handle may leak). */
495  
    template<std::size_t I>
499  
    template<std::size_t I>
496  
    void launch_one(executor_ref caller_ex, std::stop_token token)
500  
    void launch_one(executor_ref caller_ex, std::stop_token token)
497  
    {
501  
    {
498 -
        auto runner = make_when_any_runner<I>(
502 +
        auto runner = make_when_any_runner(
499 -
            std::move(std::get<I>(*tasks_)), state_);
503 +
            std::move(std::get<I>(*tasks_)), state_, I);
500  

504  

501  
        auto h = runner.release();
505  
        auto h = runner.release();
502  
        h.promise().state_ = state_;
506  
        h.promise().state_ = state_;
503  
        h.promise().index_ = I;
507  
        h.promise().index_ = I;
504  
        h.promise().env_ = io_env{caller_ex, token, state_->core_.caller_env_->frame_allocator};
508  
        h.promise().env_ = io_env{caller_ex, token, state_->core_.caller_env_->frame_allocator};
505  

509  

506  
        std::coroutine_handle<> ch{h};
510  
        std::coroutine_handle<> ch{h};
507  
        state_->runner_handles_[I] = ch;
511  
        state_->runner_handles_[I] = ch;
508  
        caller_ex.post(ch);
512  
        caller_ex.post(ch);
509  
    }
513  
    }
510  
};
514  
};
511  

515  

512  
} // namespace detail
516  
} // namespace detail
513  

517  

514  
/** Wait for the first awaitable to complete.
518  
/** Wait for the first awaitable to complete.
515  

519  

516  
    Races multiple heterogeneous awaitables concurrently and returns when the
520  
    Races multiple heterogeneous awaitables concurrently and returns when the
517 -
    first one completes. The result is a variant with one alternative per
521 +
    first one completes. The result includes the winner's index and a
518 -
    input task, preserving positional correspondence.
522 +
    deduplicated variant containing the result value.
519  

523  

520  
    @par Suspends
524  
    @par Suspends
521  
    The calling coroutine suspends when co_await is invoked. All awaitables
525  
    The calling coroutine suspends when co_await is invoked. All awaitables
522  
    are launched concurrently and execute in parallel. The coroutine resumes
526  
    are launched concurrently and execute in parallel. The coroutine resumes
523  
    only after all awaitables have completed, even though the winner is
527  
    only after all awaitables have completed, even though the winner is
524  
    determined by the first to finish.
528  
    determined by the first to finish.
525  

529  

526  
    @par Completion Conditions
530  
    @par Completion Conditions
527  
    @li Winner is determined when the first awaitable completes (success or exception)
531  
    @li Winner is determined when the first awaitable completes (success or exception)
528  
    @li Only one task can claim winner status via atomic compare-exchange
532  
    @li Only one task can claim winner status via atomic compare-exchange
529  
    @li Once a winner exists, stop is requested for all remaining siblings
533  
    @li Once a winner exists, stop is requested for all remaining siblings
530  
    @li Parent coroutine resumes only after all siblings acknowledge completion
534  
    @li Parent coroutine resumes only after all siblings acknowledge completion
531  
    @li The winner's result is returned; if the winner threw, the exception is rethrown
535  
    @li The winner's result is returned; if the winner threw, the exception is rethrown
532  

536  

533  
    @par Cancellation Semantics
537  
    @par Cancellation Semantics
534  
    Cancellation is supported via stop_token propagated through the
538  
    Cancellation is supported via stop_token propagated through the
535  
    IoAwaitable protocol:
539  
    IoAwaitable protocol:
536  
    @li Each child awaitable receives a stop_token derived from a shared stop_source
540  
    @li Each child awaitable receives a stop_token derived from a shared stop_source
537  
    @li When the parent's stop token is activated, the stop is forwarded to all children
541  
    @li When the parent's stop token is activated, the stop is forwarded to all children
538  
    @li When a winner is determined, stop_source_.request_stop() is called immediately
542  
    @li When a winner is determined, stop_source_.request_stop() is called immediately
539  
    @li Siblings must handle cancellation gracefully and complete before parent resumes
543  
    @li Siblings must handle cancellation gracefully and complete before parent resumes
540  
    @li Stop requests are cooperative; tasks must check and respond to them
544  
    @li Stop requests are cooperative; tasks must check and respond to them
541  

545  

542  
    @par Concurrency/Overlap
546  
    @par Concurrency/Overlap
543  
    All awaitables are launched concurrently before any can complete.
547  
    All awaitables are launched concurrently before any can complete.
544  
    The launcher iterates through the arguments, starting each task on the
548  
    The launcher iterates through the arguments, starting each task on the
545  
    caller's executor. Tasks may execute in parallel on multi-threaded
549  
    caller's executor. Tasks may execute in parallel on multi-threaded
546  
    executors or interleave on single-threaded executors. There is no
550  
    executors or interleave on single-threaded executors. There is no
547  
    guaranteed ordering of task completion.
551  
    guaranteed ordering of task completion.
548  

552  

549  
    @par Notable Error Conditions
553  
    @par Notable Error Conditions
550  
    @li Winner exception: if the winning task threw, that exception is rethrown
554  
    @li Winner exception: if the winning task threw, that exception is rethrown
551  
    @li Non-winner exceptions: silently discarded (only winner's result matters)
555  
    @li Non-winner exceptions: silently discarded (only winner's result matters)
552  
    @li Cancellation: tasks may complete via cancellation without throwing
556  
    @li Cancellation: tasks may complete via cancellation without throwing
553  

557  

554  
    @par Example
558  
    @par Example
555  
    @code
559  
    @code
556  
    task<void> example() {
560  
    task<void> example() {
557 -
        auto result = co_await when_any(
561 +
        auto [index, result] = co_await when_any(
 
562 +
            fetch_from_primary(),   // task<Response>
 
563 +
            fetch_from_backup()     // task<Response>
 
564 +
        );
 
565 +
        // index is 0 or 1, result holds the winner's Response
 
566 +
        auto response = std::get<Response>(result);
 
567 +
    }
 
568 +
    @endcode
 
569 +

 
570 +
    @par Example with Heterogeneous Types
 
571 +
    @code
 
572 +
    task<void> mixed_types() {
 
573 +
        auto [index, result] = co_await when_any(
558  
            fetch_int(),      // task<int>
574  
            fetch_int(),      // task<int>
559  
            fetch_string()    // task<std::string>
575  
            fetch_string()    // task<std::string>
560  
        );
576  
        );
561 -
        // result.index() is 0 or 1
577 +
        if (index == 0)
562 -
        if (result.index() == 0)
578 +
            std::cout << "Got int: " << std::get<int>(result) << "\n";
563 -
            std::cout << "Got int: " << std::get<0>(result) << "\n";
 
564  
        else
579  
        else
565 -
            std::cout << "Got string: " << std::get<1>(result) << "\n";
580 +
            std::cout << "Got string: " << std::get<std::string>(result) << "\n";
566  
    }
581  
    }
567  
    @endcode
582  
    @endcode
568  

583  

569  
    @tparam A0 First awaitable type (must satisfy IoAwaitable).
584  
    @tparam A0 First awaitable type (must satisfy IoAwaitable).
570  
    @tparam As Remaining awaitable types (must satisfy IoAwaitable).
585  
    @tparam As Remaining awaitable types (must satisfy IoAwaitable).
571  
    @param a0 The first awaitable to race.
586  
    @param a0 The first awaitable to race.
572  
    @param as Additional awaitables to race concurrently.
587  
    @param as Additional awaitables to race concurrently.
573 -
    @return A task yielding a variant with one alternative per awaitable.
588 +
    @return A task yielding a pair of (winner_index, result_variant).
574 -
        Use .index() to identify the winner. Void awaitables contribute
 
575 -
        std::monostate.
 
576  

589  

577  
    @throws Rethrows the winner's exception if the winning task threw an exception.
590  
    @throws Rethrows the winner's exception if the winning task threw an exception.
578  

591  

579  
    @par Remarks
592  
    @par Remarks
580  
    Awaitables are moved into the coroutine frame; original objects become
593  
    Awaitables are moved into the coroutine frame; original objects become
581 -
    empty after the call. The variant preserves one alternative per input
594 +
    empty after the call. When multiple awaitables share the same return type,
582 -
    task. Use .index() to determine which awaitable completed first.
595 +
    the variant is deduplicated to contain only unique types. Use the winner
583 -
    Void awaitables contribute std::monostate to the variant.
596 +
    index to determine which awaitable completed first. Void awaitables
 
597 +
    contribute std::monostate to the variant.
584  

598  

585  
    @see when_all, IoAwaitable
599  
    @see when_all, IoAwaitable
586  
*/
600  
*/
587  
template<IoAwaitable A0, IoAwaitable... As>
601  
template<IoAwaitable A0, IoAwaitable... As>
588  
[[nodiscard]] auto when_any(A0 a0, As... as)
602  
[[nodiscard]] auto when_any(A0 a0, As... as)
589 -
    -> task<detail::when_any_variant_t<
603 +
    -> task<detail::when_any_result_t<
590  
        detail::awaitable_result_t<A0>,
604  
        detail::awaitable_result_t<A0>,
591  
        detail::awaitable_result_t<As>...>>
605  
        detail::awaitable_result_t<As>...>>
592  
{
606  
{
 
607 +
    using result_type = detail::when_any_result_t<
 
608 +
        detail::awaitable_result_t<A0>,
 
609 +
        detail::awaitable_result_t<As>...>;
 
610 +

593  
    detail::when_any_state<
611  
    detail::when_any_state<
594  
        detail::awaitable_result_t<A0>,
612  
        detail::awaitable_result_t<A0>,
595  
        detail::awaitable_result_t<As>...> state;
613  
        detail::awaitable_result_t<As>...> state;
596  
    std::tuple<A0, As...> awaitable_tuple(std::move(a0), std::move(as)...);
614  
    std::tuple<A0, As...> awaitable_tuple(std::move(a0), std::move(as)...);
597  

615  

598  
    co_await detail::when_any_launcher<A0, As...>(&awaitable_tuple, &state);
616  
    co_await detail::when_any_launcher<A0, As...>(&awaitable_tuple, &state);
599  

617  

600  
    if(state.core_.winner_exception_)
618  
    if(state.core_.winner_exception_)
601  
        std::rethrow_exception(state.core_.winner_exception_);
619  
        std::rethrow_exception(state.core_.winner_exception_);
602  

620  

603 -
    co_return std::move(*state.result_);
621 +
    co_return result_type{state.core_.winner_index_, std::move(*state.result_)};
604  
}
622  
}
605  

623  

606  
/** Concept for ranges of full I/O awaitables.
624  
/** Concept for ranges of full I/O awaitables.
607  

625  

608  
    A range satisfies `IoAwaitableRange` if it is a sized input range
626  
    A range satisfies `IoAwaitableRange` if it is a sized input range
609  
    whose value type satisfies @ref IoAwaitable. This enables when_any
627  
    whose value type satisfies @ref IoAwaitable. This enables when_any
610  
    to accept any container or view of awaitables, not just std::vector.
628  
    to accept any container or view of awaitables, not just std::vector.
611  

629  

612  
    @tparam R The range type.
630  
    @tparam R The range type.
613  

631  

614  
    @par Requirements
632  
    @par Requirements
615  
    @li `R` must satisfy `std::ranges::input_range`
633  
    @li `R` must satisfy `std::ranges::input_range`
616  
    @li `R` must satisfy `std::ranges::sized_range`
634  
    @li `R` must satisfy `std::ranges::sized_range`
617  
    @li `std::ranges::range_value_t<R>` must satisfy @ref IoAwaitable
635  
    @li `std::ranges::range_value_t<R>` must satisfy @ref IoAwaitable
618  

636  

619  
    @par Syntactic Requirements
637  
    @par Syntactic Requirements
620  
    Given `r` of type `R`:
638  
    Given `r` of type `R`:
621  
    @li `std::ranges::begin(r)` is valid
639  
    @li `std::ranges::begin(r)` is valid
622  
    @li `std::ranges::end(r)` is valid
640  
    @li `std::ranges::end(r)` is valid
623  
    @li `std::ranges::size(r)` returns `std::ranges::range_size_t<R>`
641  
    @li `std::ranges::size(r)` returns `std::ranges::range_size_t<R>`
624  
    @li `*std::ranges::begin(r)` satisfies @ref IoAwaitable
642  
    @li `*std::ranges::begin(r)` satisfies @ref IoAwaitable
625  

643  

626  
    @par Example
644  
    @par Example
627  
    @code
645  
    @code
628  
    template<IoAwaitableRange R>
646  
    template<IoAwaitableRange R>
629  
    task<void> race_all(R&& awaitables) {
647  
    task<void> race_all(R&& awaitables) {
630  
        auto winner = co_await when_any(std::forward<R>(awaitables));
648  
        auto winner = co_await when_any(std::forward<R>(awaitables));
631  
        // Process winner...
649  
        // Process winner...
632  
    }
650  
    }
633  
    @endcode
651  
    @endcode
634  

652  

635  
    @see when_any, IoAwaitable
653  
    @see when_any, IoAwaitable
636  
*/
654  
*/
637  
template<typename R>
655  
template<typename R>
638  
concept IoAwaitableRange =
656  
concept IoAwaitableRange =
639  
    std::ranges::input_range<R> &&
657  
    std::ranges::input_range<R> &&
640  
    std::ranges::sized_range<R> &&
658  
    std::ranges::sized_range<R> &&
641  
    IoAwaitable<std::ranges::range_value_t<R>>;
659  
    IoAwaitable<std::ranges::range_value_t<R>>;
642  

660  

643  
namespace detail {
661  
namespace detail {
644  

662  

645  
/** Shared state for homogeneous when_any (range overload).
663  
/** Shared state for homogeneous when_any (range overload).
646  

664  

647  
    Uses composition with when_any_core for shared functionality.
665  
    Uses composition with when_any_core for shared functionality.
648  
    Simpler than heterogeneous: optional<T> instead of variant, vector
666  
    Simpler than heterogeneous: optional<T> instead of variant, vector
649  
    instead of array for runner handles.
667  
    instead of array for runner handles.
650  
*/
668  
*/
651  
template<typename T>
669  
template<typename T>
652  
struct when_any_homogeneous_state
670  
struct when_any_homogeneous_state
653  
{
671  
{
654  
    when_any_core core_;
672  
    when_any_core core_;
655  
    std::optional<T> result_;
673  
    std::optional<T> result_;
656  
    std::vector<std::coroutine_handle<>> runner_handles_;
674  
    std::vector<std::coroutine_handle<>> runner_handles_;
657  

675  

658  
    explicit when_any_homogeneous_state(std::size_t count)
676  
    explicit when_any_homogeneous_state(std::size_t count)
659  
        : core_(count)
677  
        : core_(count)
660  
        , runner_handles_(count)
678  
        , runner_handles_(count)
661  
    {
679  
    {
662  
    }
680  
    }
663  

681  

664  
    // Runners self-destruct in final_suspend. No destruction needed here.
682  
    // Runners self-destruct in final_suspend. No destruction needed here.
665  

683  

666  
    /** @pre core_.try_win() returned true. */
684  
    /** @pre core_.try_win() returned true. */
667  
    void set_winner_result(T value)
685  
    void set_winner_result(T value)
668  
        noexcept(std::is_nothrow_move_constructible_v<T>)
686  
        noexcept(std::is_nothrow_move_constructible_v<T>)
669  
    {
687  
    {
670  
        result_.emplace(std::move(value));
688  
        result_.emplace(std::move(value));
671  
    }
689  
    }
672  
};
690  
};
673  

691  

674  
/** Specialization for void tasks (no result storage needed). */
692  
/** Specialization for void tasks (no result storage needed). */
675  
template<>
693  
template<>
676  
struct when_any_homogeneous_state<void>
694  
struct when_any_homogeneous_state<void>
677  
{
695  
{
678  
    when_any_core core_;
696  
    when_any_core core_;
679  
    std::vector<std::coroutine_handle<>> runner_handles_;
697  
    std::vector<std::coroutine_handle<>> runner_handles_;
680  

698  

681  
    explicit when_any_homogeneous_state(std::size_t count)
699  
    explicit when_any_homogeneous_state(std::size_t count)
682  
        : core_(count)
700  
        : core_(count)
683  
        , runner_handles_(count)
701  
        , runner_handles_(count)
684  
    {
702  
    {
685  
    }
703  
    }
686  

704  

687  
    // Runners self-destruct in final_suspend. No destruction needed here.
705  
    // Runners self-destruct in final_suspend. No destruction needed here.
688  

706  

689  
    // No set_winner_result - void tasks have no result to store
707  
    // No set_winner_result - void tasks have no result to store
690  
};
708  
};
691  

709  

692  
/** Launches all runners concurrently; see await_suspend for lifetime concerns. */
710  
/** Launches all runners concurrently; see await_suspend for lifetime concerns. */
693  
template<IoAwaitableRange Range>
711  
template<IoAwaitableRange Range>
694  
class when_any_homogeneous_launcher
712  
class when_any_homogeneous_launcher
695  
{
713  
{
696  
    using Awaitable = std::ranges::range_value_t<Range>;
714  
    using Awaitable = std::ranges::range_value_t<Range>;
697  
    using T = awaitable_result_t<Awaitable>;
715  
    using T = awaitable_result_t<Awaitable>;
698  

716  

699  
    Range* range_;
717  
    Range* range_;
700  
    when_any_homogeneous_state<T>* state_;
718  
    when_any_homogeneous_state<T>* state_;
701  

719  

702  
public:
720  
public:
703  
    when_any_homogeneous_launcher(
721  
    when_any_homogeneous_launcher(
704  
        Range* range,
722  
        Range* range,
705  
        when_any_homogeneous_state<T>* state)
723  
        when_any_homogeneous_state<T>* state)
706  
        : range_(range)
724  
        : range_(range)
707  
        , state_(state)
725  
        , state_(state)
708  
    {
726  
    {
709  
    }
727  
    }
710  

728  

711  
    bool await_ready() const noexcept
729  
    bool await_ready() const noexcept
712  
    {
730  
    {
713  
        return std::ranges::empty(*range_);
731  
        return std::ranges::empty(*range_);
714  
    }
732  
    }
715  

733  

716  
    /** CRITICAL: If the last task finishes synchronously, parent resumes and
734  
    /** CRITICAL: If the last task finishes synchronously, parent resumes and
717  
        destroys this object before await_suspend returns. Must not reference
735  
        destroys this object before await_suspend returns. Must not reference
718  
        `this` after dispatching begins.
736  
        `this` after dispatching begins.
719  

737  

720  
        Two-phase approach:
738  
        Two-phase approach:
721  
        1. Create all runners (safe - no dispatch yet)
739  
        1. Create all runners (safe - no dispatch yet)
722  
        2. Dispatch all runners (any may complete synchronously)
740  
        2. Dispatch all runners (any may complete synchronously)
723  
    */
741  
    */
724  
    std::coroutine_handle<> await_suspend(std::coroutine_handle<> continuation, io_env const* caller_env)
742  
    std::coroutine_handle<> await_suspend(std::coroutine_handle<> continuation, io_env const* caller_env)
725  
    {
743  
    {
726  
        state_->core_.continuation_ = continuation;
744  
        state_->core_.continuation_ = continuation;
727  
        state_->core_.caller_env_ = caller_env;
745  
        state_->core_.caller_env_ = caller_env;
728  

746  

729  
        if(caller_env->stop_token.stop_possible())
747  
        if(caller_env->stop_token.stop_possible())
730  
        {
748  
        {
731  
            state_->core_.parent_stop_callback_.emplace(
749  
            state_->core_.parent_stop_callback_.emplace(
732  
                caller_env->stop_token,
750  
                caller_env->stop_token,
733  
                when_any_core::stop_callback_fn{&state_->core_.stop_source_});
751  
                when_any_core::stop_callback_fn{&state_->core_.stop_source_});
734  

752  

735  
            if(caller_env->stop_token.stop_requested())
753  
            if(caller_env->stop_token.stop_requested())
736  
                state_->core_.stop_source_.request_stop();
754  
                state_->core_.stop_source_.request_stop();
737  
        }
755  
        }
738  

756  

739  
        auto token = state_->core_.stop_source_.get_token();
757  
        auto token = state_->core_.stop_source_.get_token();
740  

758  

741  
        // Phase 1: Create all runners without dispatching.
759  
        // Phase 1: Create all runners without dispatching.
742  
        // This iterates over *range_ safely because no runners execute yet.
760  
        // This iterates over *range_ safely because no runners execute yet.
743  
        std::size_t index = 0;
761  
        std::size_t index = 0;
744  
        for(auto&& a : *range_)
762  
        for(auto&& a : *range_)
745  
        {
763  
        {
746  
            auto runner = make_when_any_runner(
764  
            auto runner = make_when_any_runner(
747  
                std::move(a), state_, index);
765  
                std::move(a), state_, index);
748  

766  

749  
            auto h = runner.release();
767  
            auto h = runner.release();
750  
            h.promise().state_ = state_;
768  
            h.promise().state_ = state_;
751  
            h.promise().index_ = index;
769  
            h.promise().index_ = index;
752  
            h.promise().env_ = io_env{caller_env->executor, token, caller_env->frame_allocator};
770  
            h.promise().env_ = io_env{caller_env->executor, token, caller_env->frame_allocator};
753  

771  

754  
            state_->runner_handles_[index] = std::coroutine_handle<>{h};
772  
            state_->runner_handles_[index] = std::coroutine_handle<>{h};
755  
            ++index;
773  
            ++index;
756  
        }
774  
        }
757  

775  

758  
        // Phase 2: Post all runners. Any may complete synchronously.
776  
        // Phase 2: Post all runners. Any may complete synchronously.
759  
        // After last post, state_ and this may be destroyed.
777  
        // After last post, state_ and this may be destroyed.
760  
        // Use raw pointer/count captured before posting.
778  
        // Use raw pointer/count captured before posting.
761  
        std::coroutine_handle<>* handles = state_->runner_handles_.data();
779  
        std::coroutine_handle<>* handles = state_->runner_handles_.data();
762  
        std::size_t count = state_->runner_handles_.size();
780  
        std::size_t count = state_->runner_handles_.size();
763  
        for(std::size_t i = 0; i < count; ++i)
781  
        for(std::size_t i = 0; i < count; ++i)
764  
            caller_env->executor.post(handles[i]);
782  
            caller_env->executor.post(handles[i]);
765  

783  

766  
        return std::noop_coroutine();
784  
        return std::noop_coroutine();
767  
    }
785  
    }
768  

786  

769  
    void await_resume() const noexcept
787  
    void await_resume() const noexcept
770  
    {
788  
    {
771  
    }
789  
    }
772  
};
790  
};
773  

791  

774  
} // namespace detail
792  
} // namespace detail
775  

793  

776  
/** Wait for the first awaitable to complete (range overload).
794  
/** Wait for the first awaitable to complete (range overload).
777  

795  

778  
    Races a range of awaitables with the same result type. Accepts any
796  
    Races a range of awaitables with the same result type. Accepts any
779  
    sized input range of IoAwaitable types, enabling use with arrays,
797  
    sized input range of IoAwaitable types, enabling use with arrays,
780  
    spans, or custom containers.
798  
    spans, or custom containers.
781  

799  

782  
    @par Suspends
800  
    @par Suspends
783  
    The calling coroutine suspends when co_await is invoked. All awaitables
801  
    The calling coroutine suspends when co_await is invoked. All awaitables
784  
    in the range are launched concurrently and execute in parallel. The
802  
    in the range are launched concurrently and execute in parallel. The
785  
    coroutine resumes only after all awaitables have completed, even though
803  
    coroutine resumes only after all awaitables have completed, even though
786  
    the winner is determined by the first to finish.
804  
    the winner is determined by the first to finish.
787  

805  

788  
    @par Completion Conditions
806  
    @par Completion Conditions
789  
    @li Winner is determined when the first awaitable completes (success or exception)
807  
    @li Winner is determined when the first awaitable completes (success or exception)
790  
    @li Only one task can claim winner status via atomic compare-exchange
808  
    @li Only one task can claim winner status via atomic compare-exchange
791  
    @li Once a winner exists, stop is requested for all remaining siblings
809  
    @li Once a winner exists, stop is requested for all remaining siblings
792  
    @li Parent coroutine resumes only after all siblings acknowledge completion
810  
    @li Parent coroutine resumes only after all siblings acknowledge completion
793  
    @li The winner's index and result are returned; if the winner threw, the exception is rethrown
811  
    @li The winner's index and result are returned; if the winner threw, the exception is rethrown
794  

812  

795  
    @par Cancellation Semantics
813  
    @par Cancellation Semantics
796  
    Cancellation is supported via stop_token propagated through the
814  
    Cancellation is supported via stop_token propagated through the
797  
    IoAwaitable protocol:
815  
    IoAwaitable protocol:
798  
    @li Each child awaitable receives a stop_token derived from a shared stop_source
816  
    @li Each child awaitable receives a stop_token derived from a shared stop_source
799  
    @li When the parent's stop token is activated, the stop is forwarded to all children
817  
    @li When the parent's stop token is activated, the stop is forwarded to all children
800  
    @li When a winner is determined, stop_source_.request_stop() is called immediately
818  
    @li When a winner is determined, stop_source_.request_stop() is called immediately
801  
    @li Siblings must handle cancellation gracefully and complete before parent resumes
819  
    @li Siblings must handle cancellation gracefully and complete before parent resumes
802  
    @li Stop requests are cooperative; tasks must check and respond to them
820  
    @li Stop requests are cooperative; tasks must check and respond to them
803  

821  

804  
    @par Concurrency/Overlap
822  
    @par Concurrency/Overlap
805  
    All awaitables are launched concurrently before any can complete.
823  
    All awaitables are launched concurrently before any can complete.
806  
    The launcher iterates through the range, starting each task on the
824  
    The launcher iterates through the range, starting each task on the
807  
    caller's executor. Tasks may execute in parallel on multi-threaded
825  
    caller's executor. Tasks may execute in parallel on multi-threaded
808  
    executors or interleave on single-threaded executors. There is no
826  
    executors or interleave on single-threaded executors. There is no
809  
    guaranteed ordering of task completion.
827  
    guaranteed ordering of task completion.
810  

828  

811  
    @par Notable Error Conditions
829  
    @par Notable Error Conditions
812  
    @li Empty range: throws std::invalid_argument immediately (not via co_return)
830  
    @li Empty range: throws std::invalid_argument immediately (not via co_return)
813  
    @li Winner exception: if the winning task threw, that exception is rethrown
831  
    @li Winner exception: if the winning task threw, that exception is rethrown
814  
    @li Non-winner exceptions: silently discarded (only winner's result matters)
832  
    @li Non-winner exceptions: silently discarded (only winner's result matters)
815  
    @li Cancellation: tasks may complete via cancellation without throwing
833  
    @li Cancellation: tasks may complete via cancellation without throwing
816  

834  

817  
    @par Example
835  
    @par Example
818  
    @code
836  
    @code
819  
    task<void> example() {
837  
    task<void> example() {
820  
        std::array<task<Response>, 3> requests = {
838  
        std::array<task<Response>, 3> requests = {
821  
            fetch_from_server(0),
839  
            fetch_from_server(0),
822  
            fetch_from_server(1),
840  
            fetch_from_server(1),
823  
            fetch_from_server(2)
841  
            fetch_from_server(2)
824  
        };
842  
        };
825  

843  

826  
        auto [index, response] = co_await when_any(std::move(requests));
844  
        auto [index, response] = co_await when_any(std::move(requests));
827  
    }
845  
    }
828  
    @endcode
846  
    @endcode
829  

847  

830  
    @par Example with Vector
848  
    @par Example with Vector
831  
    @code
849  
    @code
832  
    task<Response> fetch_fastest(std::vector<Server> const& servers) {
850  
    task<Response> fetch_fastest(std::vector<Server> const& servers) {
833  
        std::vector<task<Response>> requests;
851  
        std::vector<task<Response>> requests;
834  
        for (auto const& server : servers)
852  
        for (auto const& server : servers)
835  
            requests.push_back(fetch_from(server));
853  
            requests.push_back(fetch_from(server));
836  

854  

837  
        auto [index, response] = co_await when_any(std::move(requests));
855  
        auto [index, response] = co_await when_any(std::move(requests));
838  
        co_return response;
856  
        co_return response;
839  
    }
857  
    }
840  
    @endcode
858  
    @endcode
841  

859  

842  
    @tparam R Range type satisfying IoAwaitableRange.
860  
    @tparam R Range type satisfying IoAwaitableRange.
843  
    @param awaitables Range of awaitables to race concurrently (must not be empty).
861  
    @param awaitables Range of awaitables to race concurrently (must not be empty).
844  
    @return A task yielding a pair of (winner_index, result).
862  
    @return A task yielding a pair of (winner_index, result).
845  

863  

846  
    @throws std::invalid_argument if range is empty (thrown before coroutine suspends).
864  
    @throws std::invalid_argument if range is empty (thrown before coroutine suspends).
847  
    @throws Rethrows the winner's exception if the winning task threw an exception.
865  
    @throws Rethrows the winner's exception if the winning task threw an exception.
848  

866  

849  
    @par Remarks
867  
    @par Remarks
850  
    Elements are moved from the range; for lvalue ranges, the original
868  
    Elements are moved from the range; for lvalue ranges, the original
851  
    container will have moved-from elements after this call. The range
869  
    container will have moved-from elements after this call. The range
852  
    is moved onto the coroutine frame to ensure lifetime safety. Unlike
870  
    is moved onto the coroutine frame to ensure lifetime safety. Unlike
853  
    the variadic overload, no variant wrapper is needed since all tasks
871  
    the variadic overload, no variant wrapper is needed since all tasks
854  
    share the same return type.
872  
    share the same return type.
855  

873  

856  
    @see when_any, IoAwaitableRange
874  
    @see when_any, IoAwaitableRange
857  
*/
875  
*/
858  
template<IoAwaitableRange R>
876  
template<IoAwaitableRange R>
859  
    requires (!std::is_void_v<detail::awaitable_result_t<std::ranges::range_value_t<R>>>)
877  
    requires (!std::is_void_v<detail::awaitable_result_t<std::ranges::range_value_t<R>>>)
860  
[[nodiscard]] auto when_any(R&& awaitables)
878  
[[nodiscard]] auto when_any(R&& awaitables)
861  
    -> task<std::pair<std::size_t, detail::awaitable_result_t<std::ranges::range_value_t<R>>>>
879  
    -> task<std::pair<std::size_t, detail::awaitable_result_t<std::ranges::range_value_t<R>>>>
862  
{
880  
{
863  
    using Awaitable = std::ranges::range_value_t<R>;
881  
    using Awaitable = std::ranges::range_value_t<R>;
864  
    using T = detail::awaitable_result_t<Awaitable>;
882  
    using T = detail::awaitable_result_t<Awaitable>;
865  
    using result_type = std::pair<std::size_t, T>;
883  
    using result_type = std::pair<std::size_t, T>;
866  
    using OwnedRange = std::remove_cvref_t<R>;
884  
    using OwnedRange = std::remove_cvref_t<R>;
867  

885  

868  
    auto count = std::ranges::size(awaitables);
886  
    auto count = std::ranges::size(awaitables);
869  
    if(count == 0)
887  
    if(count == 0)
870  
        throw std::invalid_argument("when_any requires at least one awaitable");
888  
        throw std::invalid_argument("when_any requires at least one awaitable");
871  

889  

872  
    // Move/copy range onto coroutine frame to ensure lifetime
890  
    // Move/copy range onto coroutine frame to ensure lifetime
873  
    OwnedRange owned_awaitables = std::forward<R>(awaitables);
891  
    OwnedRange owned_awaitables = std::forward<R>(awaitables);
874  

892  

875  
    detail::when_any_homogeneous_state<T> state(count);
893  
    detail::when_any_homogeneous_state<T> state(count);
876  

894  

877  
    co_await detail::when_any_homogeneous_launcher<OwnedRange>(&owned_awaitables, &state);
895  
    co_await detail::when_any_homogeneous_launcher<OwnedRange>(&owned_awaitables, &state);
878  

896  

879  
    if(state.core_.winner_exception_)
897  
    if(state.core_.winner_exception_)
880  
        std::rethrow_exception(state.core_.winner_exception_);
898  
        std::rethrow_exception(state.core_.winner_exception_);
881  

899  

882  
    co_return result_type{state.core_.winner_index_, std::move(*state.result_)};
900  
    co_return result_type{state.core_.winner_index_, std::move(*state.result_)};
883  
}
901  
}
884  

902  

885  
/** Wait for the first awaitable to complete (void range overload).
903  
/** Wait for the first awaitable to complete (void range overload).
886  

904  

887  
    Races a range of void-returning awaitables. Since void awaitables have
905  
    Races a range of void-returning awaitables. Since void awaitables have
888  
    no result value, only the winner's index is returned.
906  
    no result value, only the winner's index is returned.
889  

907  

890  
    @par Suspends
908  
    @par Suspends
891  
    The calling coroutine suspends when co_await is invoked. All awaitables
909  
    The calling coroutine suspends when co_await is invoked. All awaitables
892  
    in the range are launched concurrently and execute in parallel. The
910  
    in the range are launched concurrently and execute in parallel. The
893  
    coroutine resumes only after all awaitables have completed, even though
911  
    coroutine resumes only after all awaitables have completed, even though
894  
    the winner is determined by the first to finish.
912  
    the winner is determined by the first to finish.
895  

913  

896  
    @par Completion Conditions
914  
    @par Completion Conditions
897  
    @li Winner is determined when the first awaitable completes (success or exception)
915  
    @li Winner is determined when the first awaitable completes (success or exception)
898  
    @li Only one task can claim winner status via atomic compare-exchange
916  
    @li Only one task can claim winner status via atomic compare-exchange
899  
    @li Once a winner exists, stop is requested for all remaining siblings
917  
    @li Once a winner exists, stop is requested for all remaining siblings
900  
    @li Parent coroutine resumes only after all siblings acknowledge completion
918  
    @li Parent coroutine resumes only after all siblings acknowledge completion
901  
    @li The winner's index is returned; if the winner threw, the exception is rethrown
919  
    @li The winner's index is returned; if the winner threw, the exception is rethrown
902  

920  

903  
    @par Cancellation Semantics
921  
    @par Cancellation Semantics
904  
    Cancellation is supported via stop_token propagated through the
922  
    Cancellation is supported via stop_token propagated through the
905  
    IoAwaitable protocol:
923  
    IoAwaitable protocol:
906  
    @li Each child awaitable receives a stop_token derived from a shared stop_source
924  
    @li Each child awaitable receives a stop_token derived from a shared stop_source
907  
    @li When the parent's stop token is activated, the stop is forwarded to all children
925  
    @li When the parent's stop token is activated, the stop is forwarded to all children
908  
    @li When a winner is determined, stop_source_.request_stop() is called immediately
926  
    @li When a winner is determined, stop_source_.request_stop() is called immediately
909  
    @li Siblings must handle cancellation gracefully and complete before parent resumes
927  
    @li Siblings must handle cancellation gracefully and complete before parent resumes
910  
    @li Stop requests are cooperative; tasks must check and respond to them
928  
    @li Stop requests are cooperative; tasks must check and respond to them
911  

929  

912  
    @par Concurrency/Overlap
930  
    @par Concurrency/Overlap
913  
    All awaitables are launched concurrently before any can complete.
931  
    All awaitables are launched concurrently before any can complete.
914  
    The launcher iterates through the range, starting each task on the
932  
    The launcher iterates through the range, starting each task on the
915  
    caller's executor. Tasks may execute in parallel on multi-threaded
933  
    caller's executor. Tasks may execute in parallel on multi-threaded
916  
    executors or interleave on single-threaded executors. There is no
934  
    executors or interleave on single-threaded executors. There is no
917  
    guaranteed ordering of task completion.
935  
    guaranteed ordering of task completion.
918  

936  

919  
    @par Notable Error Conditions
937  
    @par Notable Error Conditions
920  
    @li Empty range: throws std::invalid_argument immediately (not via co_return)
938  
    @li Empty range: throws std::invalid_argument immediately (not via co_return)
921  
    @li Winner exception: if the winning task threw, that exception is rethrown
939  
    @li Winner exception: if the winning task threw, that exception is rethrown
922  
    @li Non-winner exceptions: silently discarded (only winner's result matters)
940  
    @li Non-winner exceptions: silently discarded (only winner's result matters)
923  
    @li Cancellation: tasks may complete via cancellation without throwing
941  
    @li Cancellation: tasks may complete via cancellation without throwing
924  

942  

925  
    @par Example
943  
    @par Example
926  
    @code
944  
    @code
927  
    task<void> example() {
945  
    task<void> example() {
928  
        std::vector<task<void>> tasks;
946  
        std::vector<task<void>> tasks;
929  
        for (int i = 0; i < 5; ++i)
947  
        for (int i = 0; i < 5; ++i)
930  
            tasks.push_back(background_work(i));
948  
            tasks.push_back(background_work(i));
931  

949  

932  
        std::size_t winner = co_await when_any(std::move(tasks));
950  
        std::size_t winner = co_await when_any(std::move(tasks));
933  
        // winner is the index of the first task to complete
951  
        // winner is the index of the first task to complete
934  
    }
952  
    }
935  
    @endcode
953  
    @endcode
936  

954  

937  
    @par Example with Timeout
955  
    @par Example with Timeout
938  
    @code
956  
    @code
939  
    task<void> with_timeout() {
957  
    task<void> with_timeout() {
940  
        std::vector<task<void>> tasks;
958  
        std::vector<task<void>> tasks;
941  
        tasks.push_back(long_running_operation());
959  
        tasks.push_back(long_running_operation());
942  
        tasks.push_back(delay(std::chrono::seconds(5)));
960  
        tasks.push_back(delay(std::chrono::seconds(5)));
943  

961  

944  
        std::size_t winner = co_await when_any(std::move(tasks));
962  
        std::size_t winner = co_await when_any(std::move(tasks));
945  
        if (winner == 1) {
963  
        if (winner == 1) {
946  
            // Timeout occurred
964  
            // Timeout occurred
947  
        }
965  
        }
948  
    }
966  
    }
949  
    @endcode
967  
    @endcode
950  

968  

951  
    @tparam R Range type satisfying IoAwaitableRange with void result.
969  
    @tparam R Range type satisfying IoAwaitableRange with void result.
952  
    @param awaitables Range of void awaitables to race concurrently (must not be empty).
970  
    @param awaitables Range of void awaitables to race concurrently (must not be empty).
953  
    @return A task yielding the winner's index (zero-based).
971  
    @return A task yielding the winner's index (zero-based).
954  

972  

955  
    @throws std::invalid_argument if range is empty (thrown before coroutine suspends).
973  
    @throws std::invalid_argument if range is empty (thrown before coroutine suspends).
956  
    @throws Rethrows the winner's exception if the winning task threw an exception.
974  
    @throws Rethrows the winner's exception if the winning task threw an exception.
957  

975  

958  
    @par Remarks
976  
    @par Remarks
959  
    Elements are moved from the range; for lvalue ranges, the original
977  
    Elements are moved from the range; for lvalue ranges, the original
960  
    container will have moved-from elements after this call. The range
978  
    container will have moved-from elements after this call. The range
961  
    is moved onto the coroutine frame to ensure lifetime safety. Unlike
979  
    is moved onto the coroutine frame to ensure lifetime safety. Unlike
962  
    the non-void overload, no result storage is needed since void tasks
980  
    the non-void overload, no result storage is needed since void tasks
963  
    produce no value.
981  
    produce no value.
964  

982  

965  
    @see when_any, IoAwaitableRange
983  
    @see when_any, IoAwaitableRange
966  
*/
984  
*/
967  
template<IoAwaitableRange R>
985  
template<IoAwaitableRange R>
968  
    requires std::is_void_v<detail::awaitable_result_t<std::ranges::range_value_t<R>>>
986  
    requires std::is_void_v<detail::awaitable_result_t<std::ranges::range_value_t<R>>>
969  
[[nodiscard]] auto when_any(R&& awaitables) -> task<std::size_t>
987  
[[nodiscard]] auto when_any(R&& awaitables) -> task<std::size_t>
970  
{
988  
{
971  
    using OwnedRange = std::remove_cvref_t<R>;
989  
    using OwnedRange = std::remove_cvref_t<R>;
972  

990  

973  
    auto count = std::ranges::size(awaitables);
991  
    auto count = std::ranges::size(awaitables);
974  
    if(count == 0)
992  
    if(count == 0)
975  
        throw std::invalid_argument("when_any requires at least one awaitable");
993  
        throw std::invalid_argument("when_any requires at least one awaitable");
976  

994  

977  
    // Move/copy range onto coroutine frame to ensure lifetime
995  
    // Move/copy range onto coroutine frame to ensure lifetime
978  
    OwnedRange owned_awaitables = std::forward<R>(awaitables);
996  
    OwnedRange owned_awaitables = std::forward<R>(awaitables);
979  

997  

980  
    detail::when_any_homogeneous_state<void> state(count);
998  
    detail::when_any_homogeneous_state<void> state(count);
981  

999  

982  
    co_await detail::when_any_homogeneous_launcher<OwnedRange>(&owned_awaitables, &state);
1000  
    co_await detail::when_any_homogeneous_launcher<OwnedRange>(&owned_awaitables, &state);
983  

1001  

984  
    if(state.core_.winner_exception_)
1002  
    if(state.core_.winner_exception_)
985  
        std::rethrow_exception(state.core_.winner_exception_);
1003  
        std::rethrow_exception(state.core_.winner_exception_);
986  

1004  

987  
    co_return state.core_.winner_index_;
1005  
    co_return state.core_.winner_index_;
988  
}
1006  
}
989  

1007  

990  
} // namespace capy
1008  
} // namespace capy
991  
} // namespace boost
1009  
} // namespace boost
992  

1010  

993  
#endif
1011  
#endif