LCOV - code coverage report
Current view: top level - capy - when_all.hpp (source / functions) Coverage Total Hit Missed
Test: coverage_remapped.info Lines: 100.0 % 161 161
Test Date: 2026-08-14 20:51:18 Functions: 95.9 % 579 555 24

           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(result.ec)
     336                 :     {
     337                 :         state->record_error(result.ec);
     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 = result.ec;
     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              50 :         return std::tuple(extract_single_result<Is>(state)...);
     457             100 :     }(std::index_sequence_for<Ts...>{});
     458                 : }
     459                 : 
     460                 : /** Starts all homogeneous runners concurrently.
     461                 : 
     462                 :     Two-phase approach: create all runners first, then post all.
     463                 :     This avoids lifetime issues if a task completes synchronously.
     464                 : */
     465                 : template<typename Range>
     466                 : class when_all_homogeneous_launcher
     467                 : {
     468                 :     using Awaitable = std::ranges::range_value_t<Range>;
     469                 :     using PayloadT = io_result_payload_t<awaitable_result_t<Awaitable>>;
     470                 : 
     471                 :     Range* range_;
     472                 :     when_all_homogeneous_state<PayloadT>* state_;
     473                 : 
     474                 : public:
     475              16 :     when_all_homogeneous_launcher(
     476                 :         Range* range,
     477                 :         when_all_homogeneous_state<PayloadT>* state)
     478              16 :         : range_(range)
     479              16 :         , state_(state)
     480                 :     {
     481              16 :     }
     482                 : 
     483              16 :     bool await_ready() const noexcept
     484                 :     {
     485              16 :         return std::ranges::empty(*range_);
     486                 :     }
     487                 : 
     488              16 :     std::coroutine_handle<> await_suspend(std::coroutine_handle<> continuation, io_env const* caller_env)
     489                 :     {
     490              16 :         state_->core_.continuation_.h = continuation;
     491              16 :         state_->core_.caller_env_ = caller_env;
     492                 : 
     493              16 :         if(caller_env->stop_token.stop_possible())
     494                 :         {
     495               4 :             state_->core_.parent_stop_callback_.emplace(
     496               2 :                 caller_env->stop_token,
     497               2 :                 when_all_core::stop_callback_fn{&state_->core_.stop_source_});
     498                 : 
     499               2 :             if(caller_env->stop_token.stop_requested())
     500               1 :                 state_->core_.stop_source_.request_stop();
     501                 :         }
     502                 : 
     503              16 :         auto token = state_->core_.stop_source_.get_token();
     504                 : 
     505                 :         // Phase 1: Create all runners without dispatching.
     506              16 :         std::size_t index = 0;
     507              53 :         for(auto&& a : *range_)
     508                 :         {
     509              37 :             auto runner = make_when_all_homogeneous_runner(
     510              37 :                 std::move(a), state_, index);
     511                 : 
     512              37 :             auto h = runner.release();
     513              37 :             h.promise().state_ = state_;
     514              37 :             h.promise().index_ = index;
     515              37 :             h.promise().env_ = io_env{caller_env->executor, token, caller_env->frame_allocator};
     516                 : 
     517              37 :             state_->runner_handles_[index].h = std::coroutine_handle<>{h};
     518              37 :             ++index;
     519                 :         }
     520                 : 
     521                 :         // Phase 2: Post all runners. Any may complete synchronously.
     522                 :         // After last post, state_ and this may be destroyed.
     523              16 :         auto* handles = state_->runner_handles_.get();
     524              16 :         std::size_t count = state_->core_.remaining_count_.load(std::memory_order_relaxed);
     525              53 :         for(std::size_t i = 0; i < count; ++i)
     526              37 :             caller_env->executor.post(handles[i]);
     527                 : 
     528              32 :         return std::noop_coroutine();
     529              53 :     }
     530                 : 
     531              16 :     void await_resume() const noexcept
     532                 :     {
     533              16 :     }
     534                 : };
     535                 : 
     536                 : } // namespace detail
     537                 : 
     538                 : /** Execute a range of io_result-returning awaitables concurrently.
     539                 : 
     540                 :     Starts all awaitables simultaneously and waits for all to complete.
     541                 :     On success, extracted payloads are collected in a vector preserving
     542                 :     input order. The first error_code makes a stop request that every
     543                 :     sibling observes, and is propagated in the outer io_result.
     544                 :     Exceptions always beat error codes.
     545                 : 
     546                 :     @li All child awaitables run concurrently on the caller's executor.
     547                 :     @li Payloads are returned as a vector in input order.
     548                 :     @li First error_code wins and makes a stop request that siblings observe.
     549                 :     @li Exception always beats error_code.
     550                 :     @li Completes only after all children have finished.
     551                 : 
     552                 :     @par Await-effects
     553                 : 
     554                 :     Takes ownership of the range, creates one wrapper coroutine per
     555                 :     element, then posts every wrapper to the caller's executor. All
     556                 :     children therefore run concurrently, each awaited with the caller's
     557                 :     executor and frame allocator and with a stop token owned by this
     558                 :     operation.
     559                 : 
     560                 :     Awaiting an empty range throws `std::invalid_argument` before any
     561                 :     child is started.
     562                 : 
     563                 :     A stop request is made on the operation's own stop token when:
     564                 : 
     565                 :     @li a child await-returns a non-zero `ec`, or
     566                 :     @li a child exits via an exception, or
     567                 :     @li the caller's stop token is triggered.
     568                 : 
     569                 :     Every sibling observes that request through the stop token it was
     570                 :     awaited with. The request does not end the operation: the await
     571                 :     completes only after every child has finished.
     572                 : 
     573                 :     @par Await-returns
     574                 :     An object of type `io_result<std::vector<PayloadT>>` destructuring as
     575                 :     `[ec, values]`, where `PayloadT` is the payload of one child's
     576                 :     `io_result`.
     577                 : 
     578                 :     `ec` is the first non-zero `ec` await-returned by a child, in
     579                 :     completion order rather than input order. The `ec` of every other
     580                 :     child is discarded.
     581                 : 
     582                 :     On success, `values` holds one payload per element of the input
     583                 :     range, in input order. If `ec` is set, `values` is empty: the
     584                 :     payloads of the children that did succeed are discarded.
     585                 : 
     586                 :     If any child exits via an exception, the first such exception is
     587                 :     rethrown instead of await-returning, even when a child also reported
     588                 :     an `ec`.
     589                 : 
     590                 :     @par Await-postcondition
     591                 :     Every child has finished. `ec` is success only if every child
     592                 :     await-returned success. If `ec` is success, `values` holds one
     593                 :     payload per input awaitable; otherwise `values` is empty.
     594                 : 
     595                 :     @par Remarks
     596                 :     Supports _IoAwaitable cancellation_.
     597                 : 
     598                 :     @par Thread Safety
     599                 :     The returned task must be awaited from a single execution context.
     600                 :     Child awaitables execute concurrently but complete through the caller's
     601                 :     executor.
     602                 : 
     603                 :     @param awaitables Range of io_result-returning awaitables to execute
     604                 :         concurrently (must not be empty).
     605                 : 
     606                 :     @return A task yielding io_result<vector<PayloadT>> where PayloadT
     607                 :         is the payload extracted from each child's io_result.
     608                 : 
     609                 :     @throws std::invalid_argument if range is empty (thrown before
     610                 :         coroutine suspends).
     611                 : 
     612                 :     @par Exception Safety
     613                 :     If a child throws, the first child exception is rethrown after
     614                 :     all children complete (exception beats error_code).
     615                 : 
     616                 :     @par Example
     617                 :     @code
     618                 :     task<void> example()
     619                 :     {
     620                 :         std::vector<io_task<size_t>> reads;
     621                 :         for (auto& buf : buffers)
     622                 :             reads.push_back(stream.read_some(buf));
     623                 : 
     624                 :         auto [ec, counts] = co_await when_all(std::move(reads));
     625                 :         if (ec) { // handle error
     626                 :         }
     627                 :     }
     628                 :     @endcode
     629                 : 
     630                 :     @see IoAwaitableRange, when_all
     631                 : */
     632                 : template<IoAwaitableRange R>
     633                 :     requires detail::is_io_result_v<
     634                 :         awaitable_result_t<std::ranges::range_value_t<R>>>
     635                 :     && (!std::is_same_v<
     636                 :             detail::io_result_payload_t<
     637                 :                 awaitable_result_t<std::ranges::range_value_t<R>>>,
     638                 :             std::tuple<>>)
     639              14 : [[nodiscard]] auto when_all(R&& awaitables)
     640                 :     -> task<io_result<std::vector<
     641                 :         detail::io_result_payload_t<
     642                 :             awaitable_result_t<std::ranges::range_value_t<R>>>>>>
     643                 : {
     644                 :     using Awaitable = std::ranges::range_value_t<R>;
     645                 :     using PayloadT = detail::io_result_payload_t<
     646                 :         awaitable_result_t<Awaitable>>;
     647                 :     using OwnedRange = std::remove_cvref_t<R>;
     648                 : 
     649                 :     auto count = std::ranges::size(awaitables);
     650                 :     if(count == 0)
     651                 :         throw std::invalid_argument("when_all requires at least one awaitable");
     652                 : 
     653                 :     OwnedRange owned_awaitables = std::forward<R>(awaitables);
     654                 : 
     655                 :     detail::when_all_homogeneous_state<PayloadT> state(count);
     656                 : 
     657                 :     co_await detail::when_all_homogeneous_launcher<OwnedRange>(
     658                 :         &owned_awaitables, &state);
     659                 : 
     660                 :     if(state.core_.first_exception_)
     661                 :         std::rethrow_exception(state.core_.first_exception_);
     662                 : 
     663                 :     if(state.has_error_.load(std::memory_order_relaxed))
     664                 :         co_return io_result<std::vector<PayloadT>>{state.first_error_, {}};
     665                 : 
     666                 :     std::vector<PayloadT> results;
     667                 :     results.reserve(count);
     668                 :     for(auto& opt : state.results_)
     669                 :         results.push_back(std::move(*opt));
     670                 : 
     671                 :     co_return io_result<std::vector<PayloadT>>{{}, std::move(results)};
     672              28 : }
     673                 : 
     674                 : /** Execute a range of void io_result-returning awaitables concurrently.
     675                 : 
     676                 :     Starts all awaitables simultaneously and waits for all to complete.
     677                 :     Since all awaitables return io_result<>, no payload values are
     678                 :     collected. The first error_code makes a stop request that every
     679                 :     sibling observes, and is propagated. Exceptions always beat error
     680                 :     codes.
     681                 : 
     682                 :     @par Await-effects
     683                 : 
     684                 :     Takes ownership of the range, creates one wrapper coroutine per
     685                 :     element, then posts every wrapper to the caller's executor. All
     686                 :     children therefore run concurrently, each awaited with the caller's
     687                 :     executor and frame allocator and with a stop token owned by this
     688                 :     operation.
     689                 : 
     690                 :     Awaiting an empty range throws `std::invalid_argument` before any
     691                 :     child is started.
     692                 : 
     693                 :     A stop request is made on the operation's own stop token when:
     694                 : 
     695                 :     @li a child await-returns a non-zero `ec`, or
     696                 :     @li a child exits via an exception, or
     697                 :     @li the caller's stop token is triggered.
     698                 : 
     699                 :     Every sibling observes that request through the stop token it was
     700                 :     awaited with. The request does not end the operation: the await
     701                 :     completes only after every child has finished.
     702                 : 
     703                 :     @par Await-returns
     704                 :     An object of type `io_result<>` destructuring as `[ec]`. The children
     705                 :     have no payloads, so nothing else is reported.
     706                 : 
     707                 :     `ec` is the first non-zero `ec` await-returned by a child, in
     708                 :     completion order rather than input order. The `ec` of every other
     709                 :     child is discarded.
     710                 : 
     711                 :     If any child exits via an exception, the first such exception is
     712                 :     rethrown instead of await-returning, even when a child also reported
     713                 :     an `ec`.
     714                 : 
     715                 :     @par Await-postcondition
     716                 :     Every child has finished. `ec` is success only if every child
     717                 :     await-returned success.
     718                 : 
     719                 :     @par Remarks
     720                 :     Supports _IoAwaitable cancellation_.
     721                 : 
     722                 :     @par Thread Safety
     723                 :     The returned task must be awaited from a single execution context.
     724                 :     Child awaitables execute concurrently but complete through the caller's
     725                 :     executor.
     726                 : 
     727                 :     @param awaitables Range of io_result<>-returning awaitables to
     728                 :         execute concurrently (must not be empty).
     729                 : 
     730                 :     @return A task yielding io_result<> whose ec is the first child
     731                 :         error, or default-constructed on success.
     732                 : 
     733                 :     @throws std::invalid_argument if range is empty.
     734                 : 
     735                 :     @par Exception Safety
     736                 :     If a child throws, the first child exception is rethrown after
     737                 :     all children complete (exception beats error_code).
     738                 : 
     739                 :     @par Example
     740                 :     @code
     741                 :     task<void> example()
     742                 :     {
     743                 :         std::vector<io_task<>> jobs;
     744                 :         for (int i = 0; i < n; ++i)
     745                 :             jobs.push_back(process(i));
     746                 : 
     747                 :         auto [ec] = co_await when_all(std::move(jobs));
     748                 :     }
     749                 :     @endcode
     750                 : 
     751                 :     @see IoAwaitableRange, when_all
     752                 : */
     753                 : template<IoAwaitableRange R>
     754                 :     requires detail::is_io_result_v<
     755                 :         awaitable_result_t<std::ranges::range_value_t<R>>>
     756                 :     && std::is_same_v<
     757                 :             detail::io_result_payload_t<
     758                 :                 awaitable_result_t<std::ranges::range_value_t<R>>>,
     759                 :             std::tuple<>>
     760               4 : [[nodiscard]] auto when_all(R&& awaitables) -> task<io_result<>>
     761                 : {
     762                 :     using OwnedRange = std::remove_cvref_t<R>;
     763                 : 
     764                 :     auto count = std::ranges::size(awaitables);
     765                 :     if(count == 0)
     766                 :         throw std::invalid_argument("when_all requires at least one awaitable");
     767                 : 
     768                 :     OwnedRange owned_awaitables = std::forward<R>(awaitables);
     769                 : 
     770                 :     detail::when_all_homogeneous_state<std::tuple<>> state(count);
     771                 : 
     772                 :     co_await detail::when_all_homogeneous_launcher<OwnedRange>(
     773                 :         &owned_awaitables, &state);
     774                 : 
     775                 :     if(state.core_.first_exception_)
     776                 :         std::rethrow_exception(state.core_.first_exception_);
     777                 : 
     778                 :     if(state.has_error_.load(std::memory_order_relaxed))
     779                 :         co_return io_result<>{state.first_error_};
     780                 : 
     781                 :     co_return io_result<>{};
     782               8 : }
     783                 : 
     784                 : /** Execute io_result-returning awaitables concurrently, inspecting error codes.
     785                 : 
     786                 :     Overload selected when all children return io_result<Ts...>.
     787                 :     The error_code is lifted out of each child into a single outer
     788                 :     io_result. On success all values are returned; on failure the
     789                 :     first error_code wins.
     790                 : 
     791                 :     @par Await-effects
     792                 : 
     793                 :     Creates and posts one wrapper coroutine per argument to the caller's
     794                 :     executor, in argument order. All children therefore run concurrently,
     795                 :     each awaited with the caller's executor and frame allocator and with
     796                 :     a stop token owned by this operation. The overload requires at least
     797                 :     one awaitable, so there is no empty case.
     798                 : 
     799                 :     A stop request is made on the operation's own stop token when:
     800                 : 
     801                 :     @li a child await-returns a non-zero `ec`, or
     802                 :     @li a child exits via an exception, or
     803                 :     @li the caller's stop token is triggered.
     804                 : 
     805                 :     Every sibling observes that request through the stop token it was
     806                 :     awaited with. The request does not end the operation: the await
     807                 :     completes only after every child has finished.
     808                 : 
     809                 :     @par Await-returns
     810                 :     An object of type `io_result<P1, ..., Pn>` destructuring as
     811                 :     `[ec, v1, ..., vn]`, where `Pi` is the payload of the i-th child's
     812                 :     `io_result`.
     813                 : 
     814                 :     `ec` is the first non-zero `ec` await-returned by a child, in
     815                 :     completion order rather than argument order. The `ec` of every other
     816                 :     child is discarded.
     817                 : 
     818                 :     Each `vi` is the payload the i-th child itself await-returned, even
     819                 :     when that child or a sibling reported an `ec`. A failed child
     820                 :     therefore still contributes whatever payload it produced. This
     821                 :     differs from the range overloads, which discard all payloads once any
     822                 :     child fails.
     823                 : 
     824                 :     If any child exits via an exception, the first such exception is
     825                 :     rethrown instead of await-returning, even when a child also reported
     826                 :     an `ec`.
     827                 : 
     828                 :     @par Await-postcondition
     829                 :     Every child has finished. Each `vi` holds the i-th child's payload,
     830                 :     and `ec` is success only if every child await-returned success.
     831                 : 
     832                 :     @par Remarks
     833                 :     Supports _IoAwaitable cancellation_.
     834                 : 
     835                 :     @par Thread Safety
     836                 :     The returned task must be awaited from a single execution context.
     837                 :     Child awaitables execute concurrently but complete through the caller's
     838                 :     executor.
     839                 : 
     840                 :     @par Exception Safety
     841                 :     If a child throws, the first child exception is rethrown after
     842                 :     all children complete (exception beats error_code).
     843                 : 
     844                 :     @param awaitables One or more awaitables each returning
     845                 :         io_result<Ts...>.
     846                 : 
     847                 :     @return A task yielding io_result<R1, R2, ..., Rn> where each Ri
     848                 :         follows the payload flattening rules.
     849                 : */
     850                 : template<IoAwaitable... As>
     851                 :     requires (sizeof...(As) > 0)
     852                 :           && detail::all_io_result_awaitables<As...>
     853              66 : [[nodiscard]] auto when_all(As... awaitables)
     854                 :     -> task<io_result<
     855                 :         detail::io_result_payload_t<awaitable_result_t<As>>...>>
     856                 : {
     857                 :     using result_type = io_result<
     858                 :         detail::io_result_payload_t<awaitable_result_t<As>>...>;
     859                 : 
     860                 :     detail::when_all_state<awaitable_result_t<As>...> state;
     861                 :     std::tuple<As...> awaitable_tuple(std::move(awaitables)...);
     862                 : 
     863                 :     co_await detail::when_all_io_launcher<As...>(&awaitable_tuple, &state);
     864                 : 
     865                 :     // Exception always wins over error_code
     866                 :     if(state.core_.first_exception_)
     867                 :         std::rethrow_exception(state.core_.first_exception_);
     868                 : 
     869                 :     auto r = detail::build_when_all_io_result<result_type>(
     870                 :         detail::extract_results(state));
     871                 :     if(state.has_error_.load(std::memory_order_relaxed))
     872                 :         r.ec = state.first_error_;
     873                 :     co_return r;
     874             132 : }
     875                 : 
     876                 : } // namespace capy
     877                 : } // namespace boost
     878                 : 
     879                 : #endif
        

Generated by: LCOV version 2.3