TLA Line data Source code
1 : //
2 : // Copyright (c) 2026 Steve Gerbino
3 : // Copyright (c) 2026 Michael Vandeberg
4 : //
5 : // Distributed under the Boost Software License, Version 1.0. (See accompanying
6 : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7 : //
8 : // Official repository: https://github.com/cppalliance/capy
9 : //
10 :
11 : #ifndef BOOST_CAPY_WHEN_ALL_HPP
12 : #define BOOST_CAPY_WHEN_ALL_HPP
13 :
14 : #include <boost/capy/detail/config.hpp>
15 : #include <boost/capy/detail/io_result_combinators.hpp>
16 : #include <boost/capy/continuation.hpp>
17 : #include <boost/capy/concept/executor.hpp>
18 : #include <boost/capy/concept/io_awaitable.hpp>
19 : #include <coroutine>
20 : #include <boost/capy/ex/frame_alloc_mixin.hpp>
21 : #include <boost/capy/ex/io_env.hpp>
22 : #include <boost/capy/ex/frame_allocator.hpp>
23 : #include <boost/capy/task.hpp>
24 :
25 : #include <array>
26 : #include <atomic>
27 : #include <exception>
28 : #include <memory>
29 : #include <optional>
30 : #include <ranges>
31 : #include <stdexcept>
32 : #include <stop_token>
33 : #include <tuple>
34 : #include <type_traits>
35 : #include <utility>
36 : #include <vector>
37 :
38 : namespace boost {
39 : namespace capy {
40 :
41 : namespace detail {
42 :
43 : /** Holds the result of a single task within when_all.
44 : */
45 : template<typename T>
46 : struct result_holder
47 : {
48 : std::optional<T> value_;
49 :
50 HIT 119 : void set(T v)
51 : {
52 119 : value_ = std::move(v);
53 119 : }
54 :
55 105 : T get() &&
56 : {
57 105 : return std::move(*value_);
58 : }
59 : };
60 :
61 : /** Core shared state for when_all operations.
62 :
63 : Contains all members and methods common to both heterogeneous (variadic)
64 : and homogeneous (range) when_all implementations. State classes embed
65 : this via composition to avoid CRTP destructor ordering issues.
66 :
67 : @par Thread Safety
68 : Atomic operations protect exception capture and completion count.
69 : */
70 : struct when_all_core
71 : {
72 : std::atomic<std::size_t> remaining_count_;
73 :
74 : // Exception storage - first error wins, others discarded
75 : std::atomic<bool> has_exception_{false};
76 : std::exception_ptr first_exception_;
77 :
78 : std::stop_source stop_source_;
79 :
80 : // Bridges parent's stop token to our stop_source
81 : struct stop_callback_fn
82 : {
83 : std::stop_source* source_;
84 3 : void operator()() const { source_->request_stop(); }
85 : };
86 : using stop_callback_t = std::stop_callback<stop_callback_fn>;
87 : std::optional<stop_callback_t> parent_stop_callback_;
88 :
89 : continuation continuation_;
90 : io_env const* caller_env_ = nullptr;
91 :
92 82 : explicit when_all_core(std::size_t count) noexcept
93 82 : : remaining_count_(count)
94 : {
95 82 : }
96 :
97 : /** Capture an exception (first one wins). */
98 21 : void capture_exception(std::exception_ptr ep)
99 : {
100 21 : bool expected = false;
101 21 : if(has_exception_.compare_exchange_strong(
102 : expected, true, std::memory_order_relaxed))
103 19 : first_exception_ = ep;
104 21 : }
105 : };
106 :
107 : /** Shared state for heterogeneous when_all (variadic overload).
108 :
109 : @tparam Ts The result types of the tasks.
110 : */
111 : template<typename... Ts>
112 : struct when_all_state
113 : {
114 : static constexpr std::size_t task_count = sizeof...(Ts);
115 :
116 : when_all_core core_;
117 : std::tuple<result_holder<Ts>...> results_;
118 : std::array<continuation, task_count> runner_handles_{};
119 :
120 : std::atomic<bool> has_error_{false};
121 : std::error_code first_error_;
122 :
123 66 : when_all_state()
124 66 : : core_(task_count)
125 : {
126 66 : }
127 :
128 : /** Record the first error (subsequent errors are discarded). */
129 46 : void record_error(std::error_code ec)
130 : {
131 46 : bool expected = false;
132 46 : if(has_error_.compare_exchange_strong(
133 : expected, true, std::memory_order_relaxed))
134 32 : first_error_ = ec;
135 46 : }
136 : };
137 :
138 : /** Shared state for homogeneous when_all (range overload).
139 :
140 : Stores extracted io_result payloads in a vector indexed by task
141 : position. Tracks the first error_code for error propagation.
142 :
143 : @tparam T The payload type extracted from io_result.
144 : */
145 : template<typename T>
146 : struct when_all_homogeneous_state
147 : {
148 : when_all_core core_;
149 : std::vector<std::optional<T>> results_;
150 : std::unique_ptr<continuation[]> runner_handles_;
151 :
152 : std::atomic<bool> has_error_{false};
153 : std::error_code first_error_;
154 :
155 13 : explicit when_all_homogeneous_state(std::size_t count)
156 13 : : core_(count)
157 26 : , results_(count)
158 13 : , runner_handles_(std::make_unique<continuation[]>(count))
159 : {
160 13 : }
161 :
162 21 : void set_result(std::size_t index, T value)
163 : {
164 21 : results_[index].emplace(std::move(value));
165 21 : }
166 :
167 : /** Record the first error (subsequent errors are discarded). */
168 7 : void record_error(std::error_code ec)
169 : {
170 7 : bool expected = false;
171 7 : if(has_error_.compare_exchange_strong(
172 : expected, true, std::memory_order_relaxed))
173 5 : first_error_ = ec;
174 7 : }
175 : };
176 :
177 : /** Specialization for void io_result children (no payload storage). */
178 : template<>
179 : struct when_all_homogeneous_state<std::tuple<>>
180 : {
181 : when_all_core core_;
182 : std::unique_ptr<continuation[]> runner_handles_;
183 :
184 : std::atomic<bool> has_error_{false};
185 : std::error_code first_error_;
186 :
187 3 : explicit when_all_homogeneous_state(std::size_t count)
188 3 : : core_(count)
189 3 : , runner_handles_(std::make_unique<continuation[]>(count))
190 : {
191 3 : }
192 :
193 : /** Record the first error (subsequent errors are discarded). */
194 1 : void record_error(std::error_code ec)
195 : {
196 1 : bool expected = false;
197 1 : if(has_error_.compare_exchange_strong(
198 : expected, true, std::memory_order_relaxed))
199 1 : first_error_ = ec;
200 1 : }
201 : };
202 :
203 : /** Wrapper coroutine that intercepts task completion for when_all.
204 :
205 : Parameterized on StateType to work with both heterogeneous (variadic)
206 : and homogeneous (range) state types. All state types expose their
207 : shared members through a `core_` member of type when_all_core.
208 :
209 : @tparam StateType The state type (when_all_state or when_all_homogeneous_state).
210 : */
211 : template<typename StateType>
212 : struct BOOST_CAPY_CORO_DESTROY_WHEN_COMPLETE when_all_runner
213 : {
214 : struct promise_type
215 : : frame_alloc_mixin
216 : {
217 : StateType* state_ = nullptr;
218 : std::size_t index_ = 0;
219 : io_env env_;
220 :
221 174 : when_all_runner get_return_object() noexcept
222 : {
223 : return when_all_runner(
224 174 : std::coroutine_handle<promise_type>::from_promise(*this));
225 : }
226 :
227 174 : std::suspend_always initial_suspend() noexcept
228 : {
229 174 : return {};
230 : }
231 :
232 174 : auto final_suspend() noexcept
233 : {
234 : struct awaiter
235 : {
236 : promise_type* p_;
237 174 : bool await_ready() const noexcept { return false; }
238 174 : auto await_suspend(std::coroutine_handle<> h) noexcept
239 : {
240 174 : auto& core = p_->state_->core_;
241 174 : auto* counter = &core.remaining_count_;
242 174 : auto* caller_env = core.caller_env_;
243 174 : auto& cont = core.continuation_;
244 :
245 174 : h.destroy();
246 :
247 174 : auto remaining = counter->fetch_sub(1, std::memory_order_acq_rel);
248 174 : if(remaining == 1)
249 82 : return detail::symmetric_transfer(caller_env->executor.dispatch(cont));
250 92 : return detail::symmetric_transfer(std::noop_coroutine());
251 : }
252 : void await_resume() const noexcept {} // LCOV_EXCL_LINE final_suspend awaiter, never resumed
253 : };
254 174 : return awaiter{this};
255 : }
256 :
257 153 : void return_void() noexcept {}
258 :
259 21 : void unhandled_exception() noexcept
260 : {
261 21 : state_->core_.capture_exception(std::current_exception());
262 21 : state_->core_.stop_source_.request_stop();
263 21 : }
264 :
265 : template<class Awaitable>
266 : struct transform_awaiter
267 : {
268 : std::decay_t<Awaitable> a_;
269 : promise_type* p_;
270 :
271 174 : bool await_ready() { return a_.await_ready(); }
272 174 : decltype(auto) await_resume() { return a_.await_resume(); }
273 :
274 : template<class Promise>
275 174 : auto await_suspend(std::coroutine_handle<Promise> h)
276 : {
277 : using R = decltype(a_.await_suspend(h, &p_->env_));
278 : if constexpr (std::is_same_v<R, std::coroutine_handle<>>)
279 174 : return detail::symmetric_transfer(a_.await_suspend(h, &p_->env_));
280 : else
281 : return a_.await_suspend(h, &p_->env_);
282 : }
283 : };
284 :
285 : template<class Awaitable>
286 174 : auto await_transform(Awaitable&& a)
287 : {
288 : using A = std::decay_t<Awaitable>;
289 : if constexpr (IoAwaitable<A>)
290 : {
291 : return transform_awaiter<Awaitable>{
292 348 : std::forward<Awaitable>(a), this};
293 : }
294 : else
295 : {
296 : static_assert(sizeof(A) == 0, "requires IoAwaitable");
297 : }
298 174 : }
299 : };
300 :
301 : std::coroutine_handle<promise_type> h_;
302 :
303 174 : explicit when_all_runner(std::coroutine_handle<promise_type> h) noexcept
304 174 : : h_(h)
305 : {
306 174 : }
307 :
308 : // Enable move for all clang versions - some versions need it
309 : when_all_runner(when_all_runner&& other) noexcept
310 : : h_(std::exchange(other.h_, nullptr))
311 : {
312 : }
313 :
314 : when_all_runner(when_all_runner const&) = delete;
315 : when_all_runner& operator=(when_all_runner const&) = delete;
316 : when_all_runner& operator=(when_all_runner&&) = delete;
317 :
318 174 : auto release() noexcept
319 : {
320 174 : return std::exchange(h_, nullptr);
321 : }
322 : };
323 :
324 : /** Create an io_result-aware runner for a single awaitable (range path).
325 :
326 : Checks the error code, records errors and requests stop on failure,
327 : or extracts the payload on success.
328 : */
329 : template<IoAwaitable Awaitable, typename StateType>
330 : when_all_runner<StateType>
331 37 : make_when_all_homogeneous_runner(Awaitable inner, StateType* state, std::size_t index)
332 : {
333 : auto result = co_await std::move(inner);
334 :
335 : if(std::get<0>(result))
336 : {
337 : state->record_error(std::get<0>(result));
338 : state->core_.stop_source_.request_stop();
339 : }
340 : else
341 : {
342 : using PayloadT = io_result_payload_t<
343 : awaitable_result_t<Awaitable>>;
344 : if constexpr (!std::is_same_v<PayloadT, std::tuple<>>)
345 : {
346 : state->set_result(index,
347 : extract_io_payload(std::move(result)));
348 : }
349 : }
350 74 : }
351 :
352 : /** Create a runner for io_result children that requests stop on ec. */
353 : template<std::size_t Index, IoAwaitable Awaitable, typename... Ts>
354 : when_all_runner<when_all_state<Ts...>>
355 137 : make_when_all_io_runner(Awaitable inner, when_all_state<Ts...>* state)
356 : {
357 : auto result = co_await std::move(inner);
358 : auto ec = std::get<0>(result);
359 : std::get<Index>(state->results_).set(std::move(result));
360 :
361 : if(ec)
362 : {
363 : state->record_error(ec);
364 : state->core_.stop_source_.request_stop();
365 : }
366 274 : }
367 :
368 : /** Launcher that uses io_result-aware runners. */
369 : template<IoAwaitable... Awaitables>
370 : class when_all_io_launcher
371 : {
372 : using state_type = when_all_state<awaitable_result_t<Awaitables>...>;
373 :
374 : std::tuple<Awaitables...>* awaitables_;
375 : state_type* state_;
376 :
377 : public:
378 66 : when_all_io_launcher(
379 : std::tuple<Awaitables...>* awaitables,
380 : state_type* state)
381 66 : : awaitables_(awaitables)
382 66 : , state_(state)
383 : {
384 66 : }
385 :
386 66 : bool await_ready() const noexcept
387 : {
388 66 : return sizeof...(Awaitables) == 0;
389 : }
390 :
391 66 : std::coroutine_handle<> await_suspend(
392 : std::coroutine_handle<> continuation, io_env const* caller_env)
393 : {
394 66 : state_->core_.continuation_.h = continuation;
395 66 : state_->core_.caller_env_ = caller_env;
396 :
397 66 : if(caller_env->stop_token.stop_possible())
398 : {
399 4 : state_->core_.parent_stop_callback_.emplace(
400 2 : caller_env->stop_token,
401 2 : when_all_core::stop_callback_fn{&state_->core_.stop_source_});
402 :
403 2 : if(caller_env->stop_token.stop_requested())
404 1 : state_->core_.stop_source_.request_stop();
405 : }
406 :
407 66 : auto token = state_->core_.stop_source_.get_token();
408 66 : launch_all(std::index_sequence_for<Awaitables...>{},
409 : caller_env->executor, token);
410 :
411 132 : return std::noop_coroutine();
412 66 : }
413 :
414 66 : void await_resume() const noexcept {}
415 :
416 : private:
417 : template<std::size_t... Is>
418 66 : void launch_all(std::index_sequence<Is...>,
419 : executor_ref ex, std::stop_token token)
420 : {
421 66 : (..., launch_one<Is>(ex, token));
422 66 : }
423 :
424 : template<std::size_t I>
425 137 : void launch_one(executor_ref caller_ex, std::stop_token token)
426 : {
427 137 : auto runner = make_when_all_io_runner<I>(
428 137 : std::move(std::get<I>(*awaitables_)), state_);
429 :
430 137 : auto h = runner.release();
431 137 : h.promise().state_ = state_;
432 137 : h.promise().env_ = io_env{caller_ex, token,
433 137 : state_->core_.caller_env_->frame_allocator};
434 :
435 137 : state_->runner_handles_[I].h = std::coroutine_handle<>{h};
436 137 : state_->core_.caller_env_->executor.post(state_->runner_handles_[I]);
437 274 : }
438 : };
439 :
440 : /** Helper to extract a single result from state.
441 : This is a separate function to work around a GCC-11 ICE that occurs
442 : when using nested immediately-invoked lambdas with pack expansion.
443 : */
444 : template<std::size_t I, typename... Ts>
445 105 : auto extract_single_result(when_all_state<Ts...>& state)
446 : {
447 105 : return std::move(std::get<I>(state.results_)).get();
448 : }
449 :
450 : /** Extract all results from state as a tuple.
451 : */
452 : template<typename... Ts>
453 50 : auto extract_results(when_all_state<Ts...>& state)
454 : {
455 82 : return [&]<std::size_t... Is>(std::index_sequence<Is...>) {
456 : // Explicit element types: CTAD would collapse a single
457 : // io_result child via the tuple copy deduction guide
458 : return std::tuple<
459 : decltype(extract_single_result<Is>(state))...>(
460 50 : extract_single_result<Is>(state)...);
461 100 : }(std::index_sequence_for<Ts...>{});
462 : }
463 :
464 : /** Starts all homogeneous runners concurrently.
465 :
466 : Two-phase approach: create all runners first, then post all.
467 : This avoids lifetime issues if a task completes synchronously.
468 : */
469 : template<typename Range>
470 : class when_all_homogeneous_launcher
471 : {
472 : using Awaitable = std::ranges::range_value_t<Range>;
473 : using PayloadT = io_result_payload_t<awaitable_result_t<Awaitable>>;
474 :
475 : Range* range_;
476 : when_all_homogeneous_state<PayloadT>* state_;
477 :
478 : public:
479 16 : when_all_homogeneous_launcher(
480 : Range* range,
481 : when_all_homogeneous_state<PayloadT>* state)
482 16 : : range_(range)
483 16 : , state_(state)
484 : {
485 16 : }
486 :
487 16 : bool await_ready() const noexcept
488 : {
489 16 : return std::ranges::empty(*range_);
490 : }
491 :
492 16 : std::coroutine_handle<> await_suspend(std::coroutine_handle<> continuation, io_env const* caller_env)
493 : {
494 16 : state_->core_.continuation_.h = continuation;
495 16 : state_->core_.caller_env_ = caller_env;
496 :
497 16 : if(caller_env->stop_token.stop_possible())
498 : {
499 4 : state_->core_.parent_stop_callback_.emplace(
500 2 : caller_env->stop_token,
501 2 : when_all_core::stop_callback_fn{&state_->core_.stop_source_});
502 :
503 2 : if(caller_env->stop_token.stop_requested())
504 1 : state_->core_.stop_source_.request_stop();
505 : }
506 :
507 16 : auto token = state_->core_.stop_source_.get_token();
508 :
509 : // Phase 1: Create all runners without dispatching.
510 16 : std::size_t index = 0;
511 53 : for(auto&& a : *range_)
512 : {
513 37 : auto runner = make_when_all_homogeneous_runner(
514 37 : std::move(a), state_, index);
515 :
516 37 : auto h = runner.release();
517 37 : h.promise().state_ = state_;
518 37 : h.promise().index_ = index;
519 37 : h.promise().env_ = io_env{caller_env->executor, token, caller_env->frame_allocator};
520 :
521 37 : state_->runner_handles_[index].h = std::coroutine_handle<>{h};
522 37 : ++index;
523 : }
524 :
525 : // Phase 2: Post all runners. Any may complete synchronously.
526 : // After last post, state_ and this may be destroyed.
527 16 : auto* handles = state_->runner_handles_.get();
528 16 : std::size_t count = state_->core_.remaining_count_.load(std::memory_order_relaxed);
529 53 : for(std::size_t i = 0; i < count; ++i)
530 37 : caller_env->executor.post(handles[i]);
531 :
532 32 : return std::noop_coroutine();
533 53 : }
534 :
535 16 : void await_resume() const noexcept
536 : {
537 16 : }
538 : };
539 :
540 : } // namespace detail
541 :
542 : /** Execute a range of io_result-returning awaitables concurrently.
543 :
544 : Starts all awaitables simultaneously and waits for all to complete.
545 : On success, extracted payloads are collected in a vector preserving
546 : input order. The first error_code makes a stop request that every
547 : sibling observes, and is propagated in the outer io_result.
548 : Exceptions always beat error codes.
549 :
550 : @li All child awaitables run concurrently on the caller's executor.
551 : @li Payloads are returned as a vector in input order.
552 : @li First error_code wins and makes a stop request that siblings observe.
553 : @li Exception always beats error_code.
554 : @li Completes only after all children have finished.
555 :
556 : @par Await-effects
557 :
558 : Takes ownership of the range, creates one wrapper coroutine per
559 : element, then posts every wrapper to the caller's executor. All
560 : children therefore run concurrently, each awaited with the caller's
561 : executor and frame allocator and with a stop token owned by this
562 : operation.
563 :
564 : Awaiting an empty range throws `std::invalid_argument` before any
565 : child is started.
566 :
567 : A stop request is made on the operation's own stop token when:
568 :
569 : @li a child await-returns a non-zero `ec`, or
570 : @li a child exits via an exception, or
571 : @li the caller's stop token is triggered.
572 :
573 : Every sibling observes that request through the stop token it was
574 : awaited with. The request does not end the operation: the await
575 : completes only after every child has finished.
576 :
577 : @par Await-returns
578 : An object of type `io_result<std::vector<PayloadT>>` destructuring as
579 : `[ec, values]`, where `PayloadT` is the payload of one child's
580 : `io_result`.
581 :
582 : `ec` is the first non-zero `ec` await-returned by a child, in
583 : completion order rather than input order. The `ec` of every other
584 : child is discarded.
585 :
586 : On success, `values` holds one payload per element of the input
587 : range, in input order. If `ec` is set, `values` is empty: the
588 : payloads of the children that did succeed are discarded.
589 :
590 : If any child exits via an exception, the first such exception is
591 : rethrown instead of await-returning, even when a child also reported
592 : an `ec`.
593 :
594 : @par Await-postcondition
595 : Every child has finished. `ec` is success only if every child
596 : await-returned success. If `ec` is success, `values` holds one
597 : payload per input awaitable; otherwise `values` is empty.
598 :
599 : @par Remarks
600 : Supports _IoAwaitable cancellation_.
601 :
602 : @par Thread Safety
603 : The returned task must be awaited from a single execution context.
604 : Child awaitables execute concurrently but complete through the caller's
605 : executor.
606 :
607 : @param awaitables Range of io_result-returning awaitables to execute
608 : concurrently (must not be empty).
609 :
610 : @return A task yielding io_result<vector<PayloadT>> where PayloadT
611 : is the payload extracted from each child's io_result.
612 :
613 : @throws std::invalid_argument if range is empty (thrown before
614 : coroutine suspends).
615 :
616 : @par Exception Safety
617 : If a child throws, the first child exception is rethrown after
618 : all children complete (exception beats error_code).
619 :
620 : @par Example
621 : @par !example example_1
622 :
623 :
624 : @see IoAwaitableRange, when_all
625 : */
626 : template<IoAwaitableRange R>
627 : requires detail::is_io_result_v<
628 : awaitable_result_t<std::ranges::range_value_t<R>>>
629 : && (!std::is_same_v<
630 : detail::io_result_payload_t<
631 : awaitable_result_t<std::ranges::range_value_t<R>>>,
632 : std::tuple<>>)
633 14 : [[nodiscard]] auto when_all(R&& awaitables)
634 : -> task<io_result<std::vector<
635 : detail::io_result_payload_t<
636 : awaitable_result_t<std::ranges::range_value_t<R>>>>>>
637 : {
638 : using Awaitable = std::ranges::range_value_t<R>;
639 : using PayloadT = detail::io_result_payload_t<
640 : awaitable_result_t<Awaitable>>;
641 : using OwnedRange = std::remove_cvref_t<R>;
642 :
643 : auto count = std::ranges::size(awaitables);
644 : if(count == 0)
645 : throw std::invalid_argument("when_all requires at least one awaitable");
646 :
647 : OwnedRange owned_awaitables = std::forward<R>(awaitables);
648 :
649 : detail::when_all_homogeneous_state<PayloadT> state(count);
650 :
651 : co_await detail::when_all_homogeneous_launcher<OwnedRange>(
652 : &owned_awaitables, &state);
653 :
654 : if(state.core_.first_exception_)
655 : std::rethrow_exception(state.core_.first_exception_);
656 :
657 : if(state.has_error_.load(std::memory_order_relaxed))
658 : co_return io_result<std::vector<PayloadT>>{state.first_error_, {}};
659 :
660 : std::vector<PayloadT> results;
661 : results.reserve(count);
662 : for(auto& opt : state.results_)
663 : results.push_back(std::move(*opt));
664 :
665 : co_return io_result<std::vector<PayloadT>>{std::error_code(), std::move(results)};
666 28 : }
667 :
668 : /** Execute a range of void io_result-returning awaitables concurrently.
669 :
670 : Starts all awaitables simultaneously and waits for all to complete.
671 : Since all awaitables return io_result<>, no payload values are
672 : collected. The first error_code makes a stop request that every
673 : sibling observes, and is propagated. Exceptions always beat error
674 : codes.
675 :
676 : @par Await-effects
677 :
678 : Takes ownership of the range, creates one wrapper coroutine per
679 : element, then posts every wrapper to the caller's executor. All
680 : children therefore run concurrently, each awaited with the caller's
681 : executor and frame allocator and with a stop token owned by this
682 : operation.
683 :
684 : Awaiting an empty range throws `std::invalid_argument` before any
685 : child is started.
686 :
687 : A stop request is made on the operation's own stop token when:
688 :
689 : @li a child await-returns a non-zero `ec`, or
690 : @li a child exits via an exception, or
691 : @li the caller's stop token is triggered.
692 :
693 : Every sibling observes that request through the stop token it was
694 : awaited with. The request does not end the operation: the await
695 : completes only after every child has finished.
696 :
697 : @par Await-returns
698 : An object of type `io_result<>` destructuring as `[ec]`. The children
699 : have no payloads, so nothing else is reported.
700 :
701 : `ec` is the first non-zero `ec` await-returned by a child, in
702 : completion order rather than input order. The `ec` of every other
703 : child is discarded.
704 :
705 : If any child exits via an exception, the first such exception is
706 : rethrown instead of await-returning, even when a child also reported
707 : an `ec`.
708 :
709 : @par Await-postcondition
710 : Every child has finished. `ec` is success only if every child
711 : await-returned success.
712 :
713 : @par Remarks
714 : Supports _IoAwaitable cancellation_.
715 :
716 : @par Thread Safety
717 : The returned task must be awaited from a single execution context.
718 : Child awaitables execute concurrently but complete through the caller's
719 : executor.
720 :
721 : @param awaitables Range of io_result<>-returning awaitables to
722 : execute concurrently (must not be empty).
723 :
724 : @return A task yielding io_result<> whose ec is the first child
725 : error, or default-constructed on success.
726 :
727 : @throws std::invalid_argument if range is empty.
728 :
729 : @par Exception Safety
730 : If a child throws, the first child exception is rethrown after
731 : all children complete (exception beats error_code).
732 :
733 : @par Example
734 : @par !example example_2
735 :
736 :
737 : @see IoAwaitableRange, when_all
738 : */
739 : template<IoAwaitableRange R>
740 : requires detail::is_io_result_v<
741 : awaitable_result_t<std::ranges::range_value_t<R>>>
742 : && std::is_same_v<
743 : detail::io_result_payload_t<
744 : awaitable_result_t<std::ranges::range_value_t<R>>>,
745 : std::tuple<>>
746 4 : [[nodiscard]] auto when_all(R&& awaitables) -> task<io_result<>>
747 : {
748 : using OwnedRange = std::remove_cvref_t<R>;
749 :
750 : auto count = std::ranges::size(awaitables);
751 : if(count == 0)
752 : throw std::invalid_argument("when_all requires at least one awaitable");
753 :
754 : OwnedRange owned_awaitables = std::forward<R>(awaitables);
755 :
756 : detail::when_all_homogeneous_state<std::tuple<>> state(count);
757 :
758 : co_await detail::when_all_homogeneous_launcher<OwnedRange>(
759 : &owned_awaitables, &state);
760 :
761 : if(state.core_.first_exception_)
762 : std::rethrow_exception(state.core_.first_exception_);
763 :
764 : if(state.has_error_.load(std::memory_order_relaxed))
765 : co_return io_result<>{state.first_error_};
766 :
767 : co_return io_result<>{};
768 8 : }
769 :
770 : /** Execute io_result-returning awaitables concurrently, inspecting error codes.
771 :
772 : Overload selected when all children return io_result<Ts...>.
773 : The error_code is lifted out of each child into a single outer
774 : io_result. On success all values are returned; on failure the
775 : first error_code wins.
776 :
777 : @par Await-effects
778 :
779 : Creates and posts one wrapper coroutine per argument to the caller's
780 : executor, in argument order. All children therefore run concurrently,
781 : each awaited with the caller's executor and frame allocator and with
782 : a stop token owned by this operation. The overload requires at least
783 : one awaitable, so there is no empty case.
784 :
785 : A stop request is made on the operation's own stop token when:
786 :
787 : @li a child await-returns a non-zero `ec`, or
788 : @li a child exits via an exception, or
789 : @li the caller's stop token is triggered.
790 :
791 : Every sibling observes that request through the stop token it was
792 : awaited with. The request does not end the operation: the await
793 : completes only after every child has finished.
794 :
795 : @par Await-returns
796 : An object of type `io_result<P1, ..., Pn>` destructuring as
797 : `[ec, v1, ..., vn]`, where `Pi` is the payload of the i-th child's
798 : `io_result`.
799 :
800 : `ec` is the first non-zero `ec` await-returned by a child, in
801 : completion order rather than argument order. The `ec` of every other
802 : child is discarded.
803 :
804 : Each `vi` is the payload the i-th child itself await-returned, even
805 : when that child or a sibling reported an `ec`. A failed child
806 : therefore still contributes whatever payload it produced. This
807 : differs from the range overloads, which discard all payloads once any
808 : child fails.
809 :
810 : If any child exits via an exception, the first such exception is
811 : rethrown instead of await-returning, even when a child also reported
812 : an `ec`.
813 :
814 : @par Await-postcondition
815 : Every child has finished. Each `vi` holds the i-th child's payload,
816 : and `ec` is success only if every child await-returned success.
817 :
818 : @par Remarks
819 : Supports _IoAwaitable cancellation_.
820 :
821 : @par Thread Safety
822 : The returned task must be awaited from a single execution context.
823 : Child awaitables execute concurrently but complete through the caller's
824 : executor.
825 :
826 : @par Exception Safety
827 : If a child throws, the first child exception is rethrown after
828 : all children complete (exception beats error_code).
829 :
830 : @param awaitables One or more awaitables each returning
831 : io_result<Ts...>.
832 :
833 : @return A task yielding io_result<R1, R2, ..., Rn> where each Ri
834 : follows the payload flattening rules.
835 : */
836 : template<IoAwaitable... As>
837 : requires (sizeof...(As) > 0)
838 : && detail::all_io_result_awaitables<As...>
839 66 : [[nodiscard]] auto when_all(As... awaitables)
840 : -> task<io_result<
841 : detail::io_result_payload_t<awaitable_result_t<As>>...>>
842 : {
843 : using result_type = io_result<
844 : detail::io_result_payload_t<awaitable_result_t<As>>...>;
845 :
846 : detail::when_all_state<awaitable_result_t<As>...> state;
847 : std::tuple<As...> awaitable_tuple(std::move(awaitables)...);
848 :
849 : co_await detail::when_all_io_launcher<As...>(&awaitable_tuple, &state);
850 :
851 : // Exception always wins over error_code
852 : if(state.core_.first_exception_)
853 : std::rethrow_exception(state.core_.first_exception_);
854 :
855 : auto r = detail::build_when_all_io_result<result_type>(
856 : detail::extract_results(state));
857 : if(state.has_error_.load(std::memory_order_relaxed))
858 : std::get<0>(r) = state.first_error_;
859 : co_return r;
860 132 : }
861 :
862 : } // namespace capy
863 : } // namespace boost
864 :
865 : #endif
|