1  
//
1  
//
2  
// Copyright (c) 2026 Steve Gerbino
2  
// Copyright (c) 2026 Steve Gerbino
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_ALL_HPP
10  
#ifndef BOOST_CAPY_WHEN_ALL_HPP
11  
#define BOOST_CAPY_WHEN_ALL_HPP
11  
#define BOOST_CAPY_WHEN_ALL_HPP
12  

12  

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

20  

22  
#include <array>
21  
#include <array>
23  
#include <atomic>
22  
#include <atomic>
24  
#include <exception>
23  
#include <exception>
25  
#include <optional>
24  
#include <optional>
26  
#include <stop_token>
25  
#include <stop_token>
27  
#include <tuple>
26  
#include <tuple>
28  
#include <type_traits>
27  
#include <type_traits>
29  
#include <utility>
28  
#include <utility>
30  

29  

31  
namespace boost {
30  
namespace boost {
32  
namespace capy {
31  
namespace capy {
33  

32  

34  
namespace detail {
33  
namespace detail {
35  

34  

 
35 +
/** Type trait to filter void types from a tuple.
 
36 +

 
37 +
    Void-returning tasks do not contribute a value to the result tuple.
 
38 +
    This trait computes the filtered result type.
 
39 +

 
40 +
    Example: filter_void_tuple_t<int, void, string> = tuple<int, string>
 
41 +
*/
 
42 +
template<typename T>
 
43 +
using wrap_non_void_t = std::conditional_t<std::is_void_v<T>, std::tuple<>, std::tuple<T>>;
 
44 +

 
45 +
template<typename... Ts>
 
46 +
using filter_void_tuple_t = decltype(std::tuple_cat(std::declval<wrap_non_void_t<Ts>>()...));
 
47 +

36  
/** Holds the result of a single task within when_all.
48  
/** Holds the result of a single task within when_all.
37  
*/
49  
*/
38  
template<typename T>
50  
template<typename T>
39  
struct result_holder
51  
struct result_holder
40  
{
52  
{
41  
    std::optional<T> value_;
53  
    std::optional<T> value_;
42  

54  

43  
    void set(T v)
55  
    void set(T v)
44  
    {
56  
    {
45  
        value_ = std::move(v);
57  
        value_ = std::move(v);
46  
    }
58  
    }
47  

59  

48  
    T get() &&
60  
    T get() &&
49  
    {
61  
    {
50  
        return std::move(*value_);
62  
        return std::move(*value_);
51  
    }
63  
    }
52  
};
64  
};
53  

65  

54 -
/** Specialization for void tasks - returns monostate to preserve index mapping.
66 +
/** Specialization for void tasks - no value storage needed.
55  
*/
67  
*/
56  
template<>
68  
template<>
57  
struct result_holder<void>
69  
struct result_holder<void>
58 -
    std::monostate get() && { return {}; }
 
59  
{
70  
{
60  
};
71  
};
61  

72  

62  
/** Shared state for when_all operation.
73  
/** Shared state for when_all operation.
63  

74  

64  
    @tparam Ts The result types of the tasks.
75  
    @tparam Ts The result types of the tasks.
65  
*/
76  
*/
66  
template<typename... Ts>
77  
template<typename... Ts>
67  
struct when_all_state
78  
struct when_all_state
68  
{
79  
{
69  
    static constexpr std::size_t task_count = sizeof...(Ts);
80  
    static constexpr std::size_t task_count = sizeof...(Ts);
70  

81  

71  
    // Completion tracking - when_all waits for all children
82  
    // Completion tracking - when_all waits for all children
72  
    std::atomic<std::size_t> remaining_count_;
83  
    std::atomic<std::size_t> remaining_count_;
73  

84  

74  
    // Result storage in input order
85  
    // Result storage in input order
75  
    std::tuple<result_holder<Ts>...> results_;
86  
    std::tuple<result_holder<Ts>...> results_;
76  

87  

77  
    // Runner handles - destroyed in await_resume while allocator is valid
88  
    // Runner handles - destroyed in await_resume while allocator is valid
78  
    std::array<std::coroutine_handle<>, task_count> runner_handles_{};
89  
    std::array<std::coroutine_handle<>, task_count> runner_handles_{};
79  

90  

80  
    // Exception storage - first error wins, others discarded
91  
    // Exception storage - first error wins, others discarded
81  
    std::atomic<bool> has_exception_{false};
92  
    std::atomic<bool> has_exception_{false};
82  
    std::exception_ptr first_exception_;
93  
    std::exception_ptr first_exception_;
83  

94  

84  
    // Stop propagation - on error, request stop for siblings
95  
    // Stop propagation - on error, request stop for siblings
85  
    std::stop_source stop_source_;
96  
    std::stop_source stop_source_;
86  

97  

87  
    // Connects parent's stop_token to our stop_source
98  
    // Connects parent's stop_token to our stop_source
88  
    struct stop_callback_fn
99  
    struct stop_callback_fn
89  
    {
100  
    {
90  
        std::stop_source* source_;
101  
        std::stop_source* source_;
91  
        void operator()() const { source_->request_stop(); }
102  
        void operator()() const { source_->request_stop(); }
92  
    };
103  
    };
93  
    using stop_callback_t = std::stop_callback<stop_callback_fn>;
104  
    using stop_callback_t = std::stop_callback<stop_callback_fn>;
94  
    std::optional<stop_callback_t> parent_stop_callback_;
105  
    std::optional<stop_callback_t> parent_stop_callback_;
95  

106  

96  
    // Parent resumption
107  
    // Parent resumption
97  
    std::coroutine_handle<> continuation_;
108  
    std::coroutine_handle<> continuation_;
98  
    io_env const* caller_env_ = nullptr;
109  
    io_env const* caller_env_ = nullptr;
99  

110  

100  
    when_all_state()
111  
    when_all_state()
101  
        : remaining_count_(task_count)
112  
        : remaining_count_(task_count)
102  
    {
113  
    {
103  
    }
114  
    }
104  

115  

105  
    // Runners self-destruct in final_suspend. No destruction needed here.
116  
    // Runners self-destruct in final_suspend. No destruction needed here.
106  

117  

107  
    /** Capture an exception (first one wins).
118  
    /** Capture an exception (first one wins).
108  
    */
119  
    */
109  
    void capture_exception(std::exception_ptr ep)
120  
    void capture_exception(std::exception_ptr ep)
110  
    {
121  
    {
111  
        bool expected = false;
122  
        bool expected = false;
112  
        if(has_exception_.compare_exchange_strong(
123  
        if(has_exception_.compare_exchange_strong(
113  
            expected, true, std::memory_order_relaxed))
124  
            expected, true, std::memory_order_relaxed))
114  
            first_exception_ = ep;
125  
            first_exception_ = ep;
115  
    }
126  
    }
116  

127  

117  
};
128  
};
118  

129  

119  
/** Wrapper coroutine that intercepts task completion.
130  
/** Wrapper coroutine that intercepts task completion.
120  

131  

121  
    This runner awaits its assigned task and stores the result in
132  
    This runner awaits its assigned task and stores the result in
122  
    the shared state, or captures the exception and requests stop.
133  
    the shared state, or captures the exception and requests stop.
123  
*/
134  
*/
124  
template<typename T, typename... Ts>
135  
template<typename T, typename... Ts>
125  
struct when_all_runner
136  
struct when_all_runner
126  
{
137  
{
127  
    struct promise_type // : frame_allocating_base  // DISABLED FOR TESTING
138  
    struct promise_type // : frame_allocating_base  // DISABLED FOR TESTING
128  
    {
139  
    {
129  
        when_all_state<Ts...>* state_ = nullptr;
140  
        when_all_state<Ts...>* state_ = nullptr;
130  
        io_env env_;
141  
        io_env env_;
131  

142  

132  
        when_all_runner get_return_object()
143  
        when_all_runner get_return_object()
133  
        {
144  
        {
134  
            return when_all_runner(std::coroutine_handle<promise_type>::from_promise(*this));
145  
            return when_all_runner(std::coroutine_handle<promise_type>::from_promise(*this));
135  
        }
146  
        }
136  

147  

137  
        std::suspend_always initial_suspend() noexcept
148  
        std::suspend_always initial_suspend() noexcept
138  
        {
149  
        {
139  
            return {};
150  
            return {};
140  
        }
151  
        }
141  

152  

142  
        auto final_suspend() noexcept
153  
        auto final_suspend() noexcept
143  
        {
154  
        {
144  
            struct awaiter
155  
            struct awaiter
145  
            {
156  
            {
146  
                promise_type* p_;
157  
                promise_type* p_;
147  

158  

148  
                bool await_ready() const noexcept
159  
                bool await_ready() const noexcept
149  
                {
160  
                {
150  
                    return false;
161  
                    return false;
151  
                }
162  
                }
152  

163  

153  
                auto await_suspend(std::coroutine_handle<> h) noexcept
164  
                auto await_suspend(std::coroutine_handle<> h) noexcept
154  
                {
165  
                {
155  
                    // Extract everything needed before self-destruction.
166  
                    // Extract everything needed before self-destruction.
156  
                    auto* state = p_->state_;
167  
                    auto* state = p_->state_;
157  
                    auto* counter = &state->remaining_count_;
168  
                    auto* counter = &state->remaining_count_;
158  
                    auto* caller_env = state->caller_env_;
169  
                    auto* caller_env = state->caller_env_;
159  
                    auto cont = state->continuation_;
170  
                    auto cont = state->continuation_;
160  

171  

161  
                    h.destroy();
172  
                    h.destroy();
162  

173  

163  
                    // If last runner, dispatch parent for symmetric transfer.
174  
                    // If last runner, dispatch parent for symmetric transfer.
164  
                    auto remaining = counter->fetch_sub(1, std::memory_order_acq_rel);
175  
                    auto remaining = counter->fetch_sub(1, std::memory_order_acq_rel);
165  
                    if(remaining == 1)
176  
                    if(remaining == 1)
166  
                        return detail::symmetric_transfer(caller_env->executor.dispatch(cont));
177  
                        return detail::symmetric_transfer(caller_env->executor.dispatch(cont));
167  
                    return detail::symmetric_transfer(std::noop_coroutine());
178  
                    return detail::symmetric_transfer(std::noop_coroutine());
168  
                }
179  
                }
169  

180  

170  
                void await_resume() const noexcept
181  
                void await_resume() const noexcept
171  
                {
182  
                {
172  
                }
183  
                }
173  
            };
184  
            };
174  
            return awaiter{this};
185  
            return awaiter{this};
175  
        }
186  
        }
176  

187  

177  
        void return_void()
188  
        void return_void()
178  
        {
189  
        {
179  
        }
190  
        }
180  

191  

181  
        void unhandled_exception()
192  
        void unhandled_exception()
182  
        {
193  
        {
183  
            state_->capture_exception(std::current_exception());
194  
            state_->capture_exception(std::current_exception());
184  
            // Request stop for sibling tasks
195  
            // Request stop for sibling tasks
185  
            state_->stop_source_.request_stop();
196  
            state_->stop_source_.request_stop();
186  
        }
197  
        }
187  

198  

188  
        template<class Awaitable>
199  
        template<class Awaitable>
189  
        struct transform_awaiter
200  
        struct transform_awaiter
190  
        {
201  
        {
191  
            std::decay_t<Awaitable> a_;
202  
            std::decay_t<Awaitable> a_;
192  
            promise_type* p_;
203  
            promise_type* p_;
193  

204  

194  
            bool await_ready()
205  
            bool await_ready()
195  
            {
206  
            {
196  
                return a_.await_ready();
207  
                return a_.await_ready();
197  
            }
208  
            }
198  

209  

199  
            decltype(auto) await_resume()
210  
            decltype(auto) await_resume()
200  
            {
211  
            {
201  
                return a_.await_resume();
212  
                return a_.await_resume();
202  
            }
213  
            }
203  

214  

204  
            template<class Promise>
215  
            template<class Promise>
205  
            auto await_suspend(std::coroutine_handle<Promise> h)
216  
            auto await_suspend(std::coroutine_handle<Promise> h)
206  
            {
217  
            {
207  
                using R = decltype(a_.await_suspend(h, &p_->env_));
218  
                using R = decltype(a_.await_suspend(h, &p_->env_));
208  
                if constexpr (std::is_same_v<R, std::coroutine_handle<>>)
219  
                if constexpr (std::is_same_v<R, std::coroutine_handle<>>)
209  
                    return detail::symmetric_transfer(a_.await_suspend(h, &p_->env_));
220  
                    return detail::symmetric_transfer(a_.await_suspend(h, &p_->env_));
210  
                else
221  
                else
211  
                    return a_.await_suspend(h, &p_->env_);
222  
                    return a_.await_suspend(h, &p_->env_);
212  
            }
223  
            }
213  
        };
224  
        };
214  

225  

215  
        template<class Awaitable>
226  
        template<class Awaitable>
216  
        auto await_transform(Awaitable&& a)
227  
        auto await_transform(Awaitable&& a)
217  
        {
228  
        {
218  
            using A = std::decay_t<Awaitable>;
229  
            using A = std::decay_t<Awaitable>;
219  
            if constexpr (IoAwaitable<A>)
230  
            if constexpr (IoAwaitable<A>)
220  
            {
231  
            {
221  
                return transform_awaiter<Awaitable>{
232  
                return transform_awaiter<Awaitable>{
222  
                    std::forward<Awaitable>(a), this};
233  
                    std::forward<Awaitable>(a), this};
223  
            }
234  
            }
224  
            else
235  
            else
225  
            {
236  
            {
226  
                static_assert(sizeof(A) == 0, "requires IoAwaitable");
237  
                static_assert(sizeof(A) == 0, "requires IoAwaitable");
227  
            }
238  
            }
228  
        }
239  
        }
229  
    };
240  
    };
230  

241  

231  
    std::coroutine_handle<promise_type> h_;
242  
    std::coroutine_handle<promise_type> h_;
232  

243  

233  
    explicit when_all_runner(std::coroutine_handle<promise_type> h)
244  
    explicit when_all_runner(std::coroutine_handle<promise_type> h)
234  
        : h_(h)
245  
        : h_(h)
235  
    {
246  
    {
236  
    }
247  
    }
237  

248  

238  
    // Enable move for all clang versions - some versions need it
249  
    // Enable move for all clang versions - some versions need it
239  
    when_all_runner(when_all_runner&& other) noexcept : h_(std::exchange(other.h_, nullptr)) {}
250  
    when_all_runner(when_all_runner&& other) noexcept : h_(std::exchange(other.h_, nullptr)) {}
240  

251  

241  
    // Non-copyable
252  
    // Non-copyable
242  
    when_all_runner(when_all_runner const&) = delete;
253  
    when_all_runner(when_all_runner const&) = delete;
243  
    when_all_runner& operator=(when_all_runner const&) = delete;
254  
    when_all_runner& operator=(when_all_runner const&) = delete;
244  
    when_all_runner& operator=(when_all_runner&&) = delete;
255  
    when_all_runner& operator=(when_all_runner&&) = delete;
245  

256  

246  
    auto release() noexcept
257  
    auto release() noexcept
247  
    {
258  
    {
248  
        return std::exchange(h_, nullptr);
259  
        return std::exchange(h_, nullptr);
249  
    }
260  
    }
250  
};
261  
};
251  

262  

252  
/** Create a runner coroutine for a single awaitable.
263  
/** Create a runner coroutine for a single awaitable.
253  

264  

254  
    Awaitable is passed directly to ensure proper coroutine frame storage.
265  
    Awaitable is passed directly to ensure proper coroutine frame storage.
255  
*/
266  
*/
256  
template<std::size_t Index, IoAwaitable Awaitable, typename... Ts>
267  
template<std::size_t Index, IoAwaitable Awaitable, typename... Ts>
257  
when_all_runner<awaitable_result_t<Awaitable>, Ts...>
268  
when_all_runner<awaitable_result_t<Awaitable>, Ts...>
258  
make_when_all_runner(Awaitable inner, when_all_state<Ts...>* state)
269  
make_when_all_runner(Awaitable inner, when_all_state<Ts...>* state)
259  
{
270  
{
260  
    using T = awaitable_result_t<Awaitable>;
271  
    using T = awaitable_result_t<Awaitable>;
261  
    if constexpr (std::is_void_v<T>)
272  
    if constexpr (std::is_void_v<T>)
262  
    {
273  
    {
263  
        co_await std::move(inner);
274  
        co_await std::move(inner);
264  
    }
275  
    }
265  
    else
276  
    else
266  
    {
277  
    {
267  
        std::get<Index>(state->results_).set(co_await std::move(inner));
278  
        std::get<Index>(state->results_).set(co_await std::move(inner));
268  
    }
279  
    }
269  
}
280  
}
270  

281  

271  
/** Internal awaitable that launches all runner coroutines and waits.
282  
/** Internal awaitable that launches all runner coroutines and waits.
272  

283  

273  
    This awaitable is used inside the when_all coroutine to handle
284  
    This awaitable is used inside the when_all coroutine to handle
274  
    the concurrent execution of child awaitables.
285  
    the concurrent execution of child awaitables.
275  
*/
286  
*/
276  
template<IoAwaitable... Awaitables>
287  
template<IoAwaitable... Awaitables>
277  
class when_all_launcher
288  
class when_all_launcher
278  
{
289  
{
279  
    using state_type = when_all_state<awaitable_result_t<Awaitables>...>;
290  
    using state_type = when_all_state<awaitable_result_t<Awaitables>...>;
280  

291  

281  
    std::tuple<Awaitables...>* awaitables_;
292  
    std::tuple<Awaitables...>* awaitables_;
282  
    state_type* state_;
293  
    state_type* state_;
283  

294  

284  
public:
295  
public:
285  
    when_all_launcher(
296  
    when_all_launcher(
286  
        std::tuple<Awaitables...>* awaitables,
297  
        std::tuple<Awaitables...>* awaitables,
287  
        state_type* state)
298  
        state_type* state)
288  
        : awaitables_(awaitables)
299  
        : awaitables_(awaitables)
289  
        , state_(state)
300  
        , state_(state)
290  
    {
301  
    {
291  
    }
302  
    }
292  

303  

293  
    bool await_ready() const noexcept
304  
    bool await_ready() const noexcept
294  
    {
305  
    {
295  
        return sizeof...(Awaitables) == 0;
306  
        return sizeof...(Awaitables) == 0;
296  
    }
307  
    }
297  

308  

298  
    std::coroutine_handle<> await_suspend(std::coroutine_handle<> continuation, io_env const* caller_env)
309  
    std::coroutine_handle<> await_suspend(std::coroutine_handle<> continuation, io_env const* caller_env)
299  
    {
310  
    {
300  
        state_->continuation_ = continuation;
311  
        state_->continuation_ = continuation;
301  
        state_->caller_env_ = caller_env;
312  
        state_->caller_env_ = caller_env;
302  

313  

303  
        // Forward parent's stop requests to children
314  
        // Forward parent's stop requests to children
304  
        if(caller_env->stop_token.stop_possible())
315  
        if(caller_env->stop_token.stop_possible())
305  
        {
316  
        {
306  
            state_->parent_stop_callback_.emplace(
317  
            state_->parent_stop_callback_.emplace(
307  
                caller_env->stop_token,
318  
                caller_env->stop_token,
308  
                typename state_type::stop_callback_fn{&state_->stop_source_});
319  
                typename state_type::stop_callback_fn{&state_->stop_source_});
309  

320  

310  
            if(caller_env->stop_token.stop_requested())
321  
            if(caller_env->stop_token.stop_requested())
311  
                state_->stop_source_.request_stop();
322  
                state_->stop_source_.request_stop();
312  
        }
323  
        }
313  

324  

314  
        // CRITICAL: If the last task finishes synchronously then the parent
325  
        // CRITICAL: If the last task finishes synchronously then the parent
315  
        // coroutine resumes, destroying its frame, and destroying this object
326  
        // coroutine resumes, destroying its frame, and destroying this object
316  
        // prior to the completion of await_suspend. Therefore, await_suspend
327  
        // prior to the completion of await_suspend. Therefore, await_suspend
317  
        // must ensure `this` cannot be referenced after calling `launch_one`
328  
        // must ensure `this` cannot be referenced after calling `launch_one`
318  
        // for the last time.
329  
        // for the last time.
319  
        auto token = state_->stop_source_.get_token();
330  
        auto token = state_->stop_source_.get_token();
320  
        [&]<std::size_t... Is>(std::index_sequence<Is...>) {
331  
        [&]<std::size_t... Is>(std::index_sequence<Is...>) {
321  
            (..., launch_one<Is>(caller_env->executor, token));
332  
            (..., launch_one<Is>(caller_env->executor, token));
322  
        }(std::index_sequence_for<Awaitables...>{});
333  
        }(std::index_sequence_for<Awaitables...>{});
323  

334  

324  
        // Let signal_completion() handle resumption
335  
        // Let signal_completion() handle resumption
325  
        return std::noop_coroutine();
336  
        return std::noop_coroutine();
326  
    }
337  
    }
327  

338  

328  
    void await_resume() const noexcept
339  
    void await_resume() const noexcept
329  
    {
340  
    {
330  
        // Results are extracted by the when_all coroutine from state
341  
        // Results are extracted by the when_all coroutine from state
331  
    }
342  
    }
332  

343  

333  
private:
344  
private:
334  
    template<std::size_t I>
345  
    template<std::size_t I>
335  
    void launch_one(executor_ref caller_ex, std::stop_token token)
346  
    void launch_one(executor_ref caller_ex, std::stop_token token)
336  
    {
347  
    {
337  
        auto runner = make_when_all_runner<I>(
348  
        auto runner = make_when_all_runner<I>(
338  
            std::move(std::get<I>(*awaitables_)), state_);
349  
            std::move(std::get<I>(*awaitables_)), state_);
339  

350  

340  
        auto h = runner.release();
351  
        auto h = runner.release();
341  
        h.promise().state_ = state_;
352  
        h.promise().state_ = state_;
342  
        h.promise().env_ = io_env{caller_ex, token, state_->caller_env_->frame_allocator};
353  
        h.promise().env_ = io_env{caller_ex, token, state_->caller_env_->frame_allocator};
343  

354  

344  
        std::coroutine_handle<> ch{h};
355  
        std::coroutine_handle<> ch{h};
345  
        state_->runner_handles_[I] = ch;
356  
        state_->runner_handles_[I] = ch;
346  
        state_->caller_env_->executor.post(ch);
357  
        state_->caller_env_->executor.post(ch);
347  
    }
358  
    }
348  
};
359  
};
349  

360  

350 -
/** Helper to extract a single result from state.
361 +
/** Helper to extract a single result, returning empty tuple for void.
351  
    This is a separate function to work around a GCC-11 ICE that occurs
362  
    This is a separate function to work around a GCC-11 ICE that occurs
352  
    when using nested immediately-invoked lambdas with pack expansion.
363  
    when using nested immediately-invoked lambdas with pack expansion.
353  
*/
364  
*/
354  
template<std::size_t I, typename... Ts>
365  
template<std::size_t I, typename... Ts>
355  
auto extract_single_result(when_all_state<Ts...>& state)
366  
auto extract_single_result(when_all_state<Ts...>& state)
356  
{
367  
{
357 -
    return std::move(std::get<I>(state.results_)).get();
368 +
    using T = std::tuple_element_t<I, std::tuple<Ts...>>;
 
369 +
    if constexpr (std::is_void_v<T>)
 
370 +
        return std::tuple<>();
 
371 +
    else
 
372 +
        return std::make_tuple(std::move(std::get<I>(state.results_)).get());
358  
}
373  
}
359  

374  

360 -
/** Extract all results from state as a tuple.
375 +
/** Extract results from state, filtering void types.
361  
*/
376  
*/
362  
template<typename... Ts>
377  
template<typename... Ts>
363  
auto extract_results(when_all_state<Ts...>& state)
378  
auto extract_results(when_all_state<Ts...>& state)
364  
{
379  
{
365  
    return [&]<std::size_t... Is>(std::index_sequence<Is...>) {
380  
    return [&]<std::size_t... Is>(std::index_sequence<Is...>) {
366 -
        return std::tuple(extract_single_result<Is>(state)...);
381 +
        return std::tuple_cat(extract_single_result<Is>(state)...);
367  
    }(std::index_sequence_for<Ts...>{});
382  
    }(std::index_sequence_for<Ts...>{});
368  
}
383  
}
369  

384  

370  
} // namespace detail
385  
} // namespace detail
371  

386  

372 -
/** Compute the when_all result tuple type.
387 +
/** Compute a tuple type with void types filtered out.
373  

388  

374 -
    Void-returning tasks contribute std::monostate to preserve the
389 +
    Returns void when all types are void (P2300 aligned),
375 -
    task-index-to-result-index mapping, matching when_any's approach.
390 +
    otherwise returns a std::tuple with void types removed.
376  

391  

377 -
    Example: when_all_result_t<int, void, string> = std::tuple<int, std::monostate, string>
392 +
    Example: non_void_tuple_t<int, void, string> = std::tuple<int, string>
378 -
    Example: when_all_result_t<void, void> = std::tuple<std::monostate, std::monostate>
393 +
    Example: non_void_tuple_t<void, void> = void
379  
*/
394  
*/
380  
template<typename... Ts>
395  
template<typename... Ts>
381 -
using when_all_result_t = std::tuple<void_to_monostate_t<Ts>...>;
396 +
using non_void_tuple_t = std::conditional_t<
 
397 +
    std::is_same_v<detail::filter_void_tuple_t<Ts...>, std::tuple<>>,
 
398 +
    void,
 
399 +
    detail::filter_void_tuple_t<Ts...>>;
382  

400  

383  
/** Execute multiple awaitables concurrently and collect their results.
401  
/** Execute multiple awaitables concurrently and collect their results.
384  

402  

385  
    Launches all awaitables simultaneously and waits for all to complete
403  
    Launches all awaitables simultaneously and waits for all to complete
386  
    before returning. Results are collected in input order. If any
404  
    before returning. Results are collected in input order. If any
387  
    awaitable throws, cancellation is requested for siblings and the first
405  
    awaitable throws, cancellation is requested for siblings and the first
388  
    exception is rethrown after all awaitables complete.
406  
    exception is rethrown after all awaitables complete.
389  

407  

390  
    @li All child awaitables run concurrently on the caller's executor
408  
    @li All child awaitables run concurrently on the caller's executor
391  
    @li Results are returned as a tuple in input order
409  
    @li Results are returned as a tuple in input order
392 -
    @li Void-returning awaitables contribute std::monostate to the
410 +
    @li Void-returning awaitables do not contribute to the result tuple
393 -
        result tuple, preserving the task-index-to-result-index mapping
411 +
    @li If all awaitables return void, `when_all` returns `task<void>`
394  
    @li First exception wins; subsequent exceptions are discarded
412  
    @li First exception wins; subsequent exceptions are discarded
395  
    @li Stop is requested for siblings on first error
413  
    @li Stop is requested for siblings on first error
396  
    @li Completes only after all children have finished
414  
    @li Completes only after all children have finished
397  

415  

398  
    @par Thread Safety
416  
    @par Thread Safety
399  
    The returned task must be awaited from a single execution context.
417  
    The returned task must be awaited from a single execution context.
400  
    Child awaitables execute concurrently but complete through the caller's
418  
    Child awaitables execute concurrently but complete through the caller's
401  
    executor.
419  
    executor.
402  

420  

403  
    @param awaitables The awaitables to execute concurrently. Each must
421  
    @param awaitables The awaitables to execute concurrently. Each must
404  
        satisfy @ref IoAwaitable and is consumed (moved-from) when
422  
        satisfy @ref IoAwaitable and is consumed (moved-from) when
405  
        `when_all` is awaited.
423  
        `when_all` is awaited.
406  

424  

407 -
    @return A task yielding a tuple of results in input order. Void tasks
425 +
    @return A task yielding a tuple of non-void results. Returns
408 -
        contribute std::monostate to preserve index correspondence.
426 +
        `task<void>` when all input awaitables return void.
409  

427  

410  
    @par Example
428  
    @par Example
411  

429  

412  
    @code
430  
    @code
413  
    task<> example()
431  
    task<> example()
414  
    {
432  
    {
415  
        // Concurrent fetch, results collected in order
433  
        // Concurrent fetch, results collected in order
416  
        auto [user, posts] = co_await when_all(
434  
        auto [user, posts] = co_await when_all(
417  
            fetch_user( id ),      // task<User>
435  
            fetch_user( id ),      // task<User>
418  
            fetch_posts( id )      // task<std::vector<Post>>
436  
            fetch_posts( id )      // task<std::vector<Post>>
419  
        );
437  
        );
420  

438  

421 -
        // Void awaitables contribute monostate
439 +
        // Void awaitables don't contribute to result
422 -
        auto [a, _, b] = co_await when_all(
440 +
        co_await when_all(
423 -
            fetch_int(),           // task<int>
441 +
            log_event( "start" ),  // task<void>
424 -
            log_event( "start" ),  // task<void>  → monostate
442 +
            notify_user( id )      // task<void>
425 -
            fetch_str()            // task<string>
 
426  
        );
443  
        );
427 -
        // a is int, _ is monostate, b is string
444 +
        // Returns task<void>, no result tuple
428  
    }
445  
    }
429  
    @endcode
446  
    @endcode
430  

447  

431  
    @see IoAwaitable, task
448  
    @see IoAwaitable, task
432  
*/
449  
*/
433  
template<IoAwaitable... As>
450  
template<IoAwaitable... As>
434  
[[nodiscard]] auto when_all(As... awaitables)
451  
[[nodiscard]] auto when_all(As... awaitables)
435 -
    -> task<when_all_result_t<awaitable_result_t<As>...>>
452 +
    -> task<non_void_tuple_t<awaitable_result_t<As>...>>
436  
{
453  
{
 
454 +
    using result_type = non_void_tuple_t<awaitable_result_t<As>...>;
 
455 +

437  
    // State is stored in the coroutine frame, using the frame allocator
456  
    // State is stored in the coroutine frame, using the frame allocator
438  
    detail::when_all_state<awaitable_result_t<As>...> state;
457  
    detail::when_all_state<awaitable_result_t<As>...> state;
439  

458  

440  
    // Store awaitables in the frame
459  
    // Store awaitables in the frame
441  
    std::tuple<As...> awaitable_tuple(std::move(awaitables)...);
460  
    std::tuple<As...> awaitable_tuple(std::move(awaitables)...);
442  

461  

443  
    // Launch all awaitables and wait for completion
462  
    // Launch all awaitables and wait for completion
444  
    co_await detail::when_all_launcher<As...>(&awaitable_tuple, &state);
463  
    co_await detail::when_all_launcher<As...>(&awaitable_tuple, &state);
445  

464  

446  
    // Propagate first exception if any.
465  
    // Propagate first exception if any.
447  
    // Safe without explicit acquire: capture_exception() is sequenced-before
466  
    // Safe without explicit acquire: capture_exception() is sequenced-before
448  
    // signal_completion()'s acq_rel fetch_sub, which synchronizes-with the
467  
    // signal_completion()'s acq_rel fetch_sub, which synchronizes-with the
449  
    // last task's decrement that resumes this coroutine.
468  
    // last task's decrement that resumes this coroutine.
450  
    if(state.first_exception_)
469  
    if(state.first_exception_)
451  
        std::rethrow_exception(state.first_exception_);
470  
        std::rethrow_exception(state.first_exception_);
452  

471  

453 -
    co_return detail::extract_results(state);
472 +
    // Extract and return results
 
473 +
    if constexpr (std::is_void_v<result_type>)
 
474 +
        co_return;
 
475 +
    else
 
476 +
        co_return detail::extract_results(state);
454  
}
477  
}
455  

478  

456  
} // namespace capy
479  
} // namespace capy
457  
} // namespace boost
480  
} // namespace boost
458  

481  

459  
#endif
482  
#endif