LCOV - code coverage report
Current view: top level - boost/capy/ex - run_async.hpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 91.9 % 86 79
Test Date: 2026-01-18 20:48:05 Functions: 82.1 % 909 746

            Line data    Source code
       1              : //
       2              : // Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com)
       3              : //
       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)
       6              : //
       7              : // Official repository: https://github.com/cppalliance/capy
       8              : //
       9              : 
      10              : #ifndef BOOST_CAPY_RUN_ASYNC_HPP
      11              : #define BOOST_CAPY_RUN_ASYNC_HPP
      12              : 
      13              : #include <boost/capy/detail/config.hpp>
      14              : #include <boost/capy/concept/executor.hpp>
      15              : #include <boost/capy/concept/frame_allocator.hpp>
      16              : #include <boost/capy/task.hpp>
      17              : 
      18              : #include <concepts>
      19              : #include <coroutine>
      20              : #include <exception>
      21              : #include <optional>
      22              : #include <stop_token>
      23              : #include <type_traits>
      24              : #include <utility>
      25              : 
      26              : namespace boost {
      27              : namespace capy {
      28              : 
      29              : //----------------------------------------------------------
      30              : //
      31              : // Handler Types
      32              : //
      33              : //----------------------------------------------------------
      34              : 
      35              : /** Default handler for run_async that discards results and rethrows exceptions.
      36              : 
      37              :     This handler type is used when no user-provided handlers are specified.
      38              :     On successful completion it discards the result value. On exception it
      39              :     rethrows the exception from the exception_ptr.
      40              : 
      41              :     @par Thread Safety
      42              :     All member functions are thread-safe.
      43              : 
      44              :     @see run_async
      45              :     @see handler_pair
      46              : */
      47              : struct default_handler
      48              : {
      49              :     /// Discard a non-void result value.
      50              :     template<class T>
      51            1 :     void operator()(T&&) const noexcept
      52              :     {
      53            1 :     }
      54              : 
      55              :     /// Handle void result (no-op).
      56            1 :     void operator()() const noexcept
      57              :     {
      58            1 :     }
      59              : 
      60              :     /// Rethrow the captured exception.
      61            0 :     void operator()(std::exception_ptr ep) const
      62              :     {
      63            0 :         if(ep)
      64            0 :             std::rethrow_exception(ep);
      65            0 :     }
      66              : };
      67              : 
      68              : /** Combines two handlers into one: h1 for success, h2 for exception.
      69              : 
      70              :     This class template wraps a success handler and an error handler,
      71              :     providing a unified callable interface for the trampoline coroutine.
      72              : 
      73              :     @tparam H1 The success handler type. Must be invocable with `T&&` for
      74              :                non-void tasks or with no arguments for void tasks.
      75              :     @tparam H2 The error handler type. Must be invocable with `std::exception_ptr`.
      76              : 
      77              :     @par Thread Safety
      78              :     Thread safety depends on the contained handlers.
      79              : 
      80              :     @see run_async
      81              :     @see default_handler
      82              : */
      83              : template<class H1, class H2>
      84              : struct handler_pair
      85              : {
      86              :     H1 h1_;
      87              :     H2 h2_;
      88              : 
      89              :     /// Invoke success handler with non-void result.
      90              :     template<class T>
      91           24 :     void operator()(T&& v)
      92              :     {
      93           24 :         h1_(std::forward<T>(v));
      94           24 :     }
      95              : 
      96              :     /// Invoke success handler for void result.
      97            2 :     void operator()()
      98              :     {
      99            2 :         h1_();
     100            2 :     }
     101              : 
     102              :     /// Invoke error handler with exception.
     103           12 :     void operator()(std::exception_ptr ep)
     104              :     {
     105           12 :         h2_(ep);
     106           12 :     }
     107              : };
     108              : 
     109              : /** Specialization for single handler that may handle both success and error.
     110              : 
     111              :     When only one handler is provided to `run_async`, this specialization
     112              :     checks at compile time whether the handler can accept `std::exception_ptr`.
     113              :     If so, it routes exceptions to the handler. Otherwise, exceptions are
     114              :     rethrown (the default behavior).
     115              : 
     116              :     @tparam H1 The handler type. If invocable with `std::exception_ptr`,
     117              :                it handles both success and error cases.
     118              : 
     119              :     @par Thread Safety
     120              :     Thread safety depends on the contained handler.
     121              : 
     122              :     @see run_async
     123              :     @see default_handler
     124              : */
     125              : template<class H1>
     126              : struct handler_pair<H1, default_handler>
     127              : {
     128              :     H1 h1_;
     129              : 
     130              :     /// Invoke handler with non-void result.
     131              :     template<class T>
     132           16 :     void operator()(T&& v)
     133              :     {
     134           16 :         h1_(std::forward<T>(v));
     135           16 :     }
     136              : 
     137              :     /// Invoke handler for void result.
     138            1 :     void operator()()
     139              :     {
     140            1 :         h1_();
     141            1 :     }
     142              : 
     143              :     /// Route exception to h1 if it accepts exception_ptr, otherwise rethrow.
     144            1 :     void operator()(std::exception_ptr ep)
     145              :     {
     146              :         if constexpr(std::invocable<H1, std::exception_ptr>)
     147            1 :             h1_(ep);
     148              :         else
     149            0 :             std::rethrow_exception(ep);
     150            1 :     }
     151              : };
     152              : 
     153              : namespace detail {
     154              : 
     155              : //----------------------------------------------------------
     156              : //
     157              : // Trampoline Coroutine
     158              : //
     159              : //----------------------------------------------------------
     160              : 
     161              : /// Awaiter to access the promise from within the coroutine.
     162              : template<class Promise>
     163              : struct get_promise_awaiter
     164              : {
     165              :     Promise* p_ = nullptr;
     166              : 
     167           58 :     bool await_ready() const noexcept { return false; }
     168              : 
     169           58 :     bool await_suspend(std::coroutine_handle<Promise> h) noexcept
     170              :     {
     171           58 :         p_ = &h.promise();
     172           58 :         return false;
     173              :     }
     174              : 
     175           58 :     Promise& await_resume() const noexcept
     176              :     {
     177           58 :         return *p_;
     178              :     }
     179              : };
     180              : 
     181              : /** Internal trampoline coroutine for run_async.
     182              : 
     183              :     The trampoline is allocated BEFORE the task (via C++17 postfix evaluation
     184              :     order) and serves as the task's continuation. When the task final_suspends,
     185              :     control returns to the trampoline which then invokes the appropriate handler.
     186              : 
     187              :     @tparam Handlers The handler type (default_handler or handler_pair).
     188              : */
     189              : template<class Handlers>
     190              : struct trampoline
     191              : {
     192              :     using invoke_fn = void(*)(void*, std::optional<Handlers>&);
     193              : 
     194              :     struct promise_type
     195              :     {
     196              :         invoke_fn invoke_ = nullptr;
     197              :         void* task_promise_ = nullptr;
     198              :         std::optional<Handlers> handlers_;
     199              :         std::coroutine_handle<> task_h_;
     200              : 
     201           58 :         trampoline get_return_object() noexcept
     202              :         {
     203              :             return trampoline{
     204           58 :                 std::coroutine_handle<promise_type>::from_promise(*this)};
     205              :         }
     206              : 
     207           58 :         std::suspend_always initial_suspend() noexcept
     208              :         {
     209           58 :             return {};
     210              :         }
     211              : 
     212              :         // Self-destruct after invoking handlers
     213           58 :         std::suspend_never final_suspend() noexcept
     214              :         {
     215           58 :             return {};
     216              :         }
     217              : 
     218           58 :         void return_void() noexcept
     219              :         {
     220           58 :         }
     221              : 
     222            0 :         void unhandled_exception() noexcept
     223              :         {
     224              :             // Handler threw - this is undefined behavior if no error handler provided
     225            0 :         }
     226              :     };
     227              : 
     228              :     std::coroutine_handle<promise_type> h_;
     229              : 
     230              :     /// Type-erased invoke function instantiated per task<T>.
     231              :     template<class T>
     232           58 :     static void invoke_impl(void* p, std::optional<Handlers>& h)
     233              :     {
     234           58 :         auto& promise = *static_cast<typename task<T>::promise_type*>(p);
     235           58 :         if(promise.ep_)
     236           13 :             (*h)(promise.ep_);
     237              :         else if constexpr(std::is_void_v<T>)
     238            4 :             (*h)();
     239              :         else
     240           41 :             (*h)(std::move(*promise.result_));
     241           58 :     }
     242              : };
     243              : 
     244              : /// Coroutine body for trampoline - invokes handlers then destroys task.
     245              : template<class Handlers>
     246              : trampoline<Handlers>
     247           58 : make_trampoline()
     248              : {
     249              :     auto& p = co_await get_promise_awaiter<typename trampoline<Handlers>::promise_type>{};
     250              :     
     251              :     // Invoke the type-erased handler
     252              :     p.invoke_(p.task_promise_, p.handlers_);
     253              :     
     254              :     // Destroy task (LIFO: task destroyed first, trampoline destroyed after)
     255              :     p.task_h_.destroy();
     256          116 : }
     257              : 
     258              : } // namespace detail
     259              : 
     260              : //----------------------------------------------------------
     261              : //
     262              : // run_async_wrapper
     263              : //
     264              : //----------------------------------------------------------
     265              : 
     266              : /** Wrapper returned by run_async that accepts a task for execution.
     267              : 
     268              :     This wrapper holds the trampoline coroutine, executor, stop token,
     269              :     and handlers. The trampoline is allocated when the wrapper is constructed
     270              :     (before the task due to C++17 postfix evaluation order).
     271              : 
     272              :     The rvalue ref-qualifier on `operator()` ensures the wrapper can only
     273              :     be used as a temporary, preventing misuse that would violate LIFO ordering.
     274              : 
     275              :     @tparam Ex The executor type satisfying the `Executor` concept.
     276              :     @tparam Handlers The handler type (default_handler or handler_pair).
     277              : 
     278              :     @par Thread Safety
     279              :     The wrapper itself should only be used from one thread. The handlers
     280              :     may be invoked from any thread where the executor schedules work.
     281              : 
     282              :     @par Example
     283              :     @code
     284              :     // Correct usage - wrapper is temporary
     285              :     run_async(ex)(my_task());
     286              : 
     287              :     // Compile error - cannot call operator() on lvalue
     288              :     auto w = run_async(ex);
     289              :     w(my_task());  // Error: operator() requires rvalue
     290              :     @endcode
     291              : 
     292              :     @see run_async
     293              : */
     294              : template<Executor Ex, class Handlers>
     295              : class [[nodiscard]] run_async_wrapper
     296              : {
     297              :     detail::trampoline<Handlers> tr_;
     298              :     Ex ex_;
     299              :     std::stop_token st_;
     300              : 
     301              : public:
     302              :     /// Construct wrapper with executor, stop token, and handlers.
     303           58 :     run_async_wrapper(
     304              :         Ex ex,
     305              :         std::stop_token st,
     306              :         Handlers h)
     307           58 :         : tr_(detail::make_trampoline<Handlers>())
     308           58 :         , ex_(std::move(ex))
     309           58 :         , st_(std::move(st))
     310              :     {
     311              :         // Store handlers in the trampoline's promise
     312           58 :         tr_.h_.promise().handlers_.emplace(std::move(h));
     313           58 :     }
     314              : 
     315              :     // Non-copyable, non-movable (must be used immediately)
     316              :     run_async_wrapper(run_async_wrapper const&) = delete;
     317              :     run_async_wrapper& operator=(run_async_wrapper const&) = delete;
     318              :     run_async_wrapper(run_async_wrapper&&) = delete;
     319              :     run_async_wrapper& operator=(run_async_wrapper&&) = delete;
     320              : 
     321              :     /** Launch the task for execution.
     322              : 
     323              :         This operator accepts a task and launches it on the executor.
     324              :         The rvalue ref-qualifier ensures the wrapper is consumed, enforcing
     325              :         correct LIFO destruction order.
     326              : 
     327              :         @tparam T The task's return type.
     328              : 
     329              :         @param t The task to execute. Ownership is transferred to the
     330              :                  trampoline which will destroy it after completion.
     331              :     */
     332              :     template<class T>
     333           58 :     void operator()(task<T> t) &&
     334              :     {
     335           58 :         auto task_h = t.release();
     336           58 :         auto& p = tr_.h_.promise();
     337              : 
     338              :         // Inject T-specific invoke function
     339           58 :         p.invoke_ = detail::trampoline<Handlers>::template invoke_impl<T>;
     340           58 :         p.task_promise_ = &task_h.promise();
     341           58 :         p.task_h_ = task_h;
     342              : 
     343              :         // Setup task's continuation to return to trampoline
     344           58 :         task_h.promise().continuation_ = tr_.h_;
     345           58 :         task_h.promise().caller_ex_ = ex_;
     346           58 :         task_h.promise().ex_ = ex_;
     347           58 :         task_h.promise().set_stop_token(st_);
     348              : 
     349              :         // Resume task through executor
     350              :         // The executor returns a handle for symmetric transfer;
     351              :         // from non-coroutine code we must explicitly resume it
     352           58 :         ex_.dispatch(task_h)();
     353           58 :     }
     354              : };
     355              : 
     356              : //----------------------------------------------------------
     357              : //
     358              : // run_async Overloads
     359              : //
     360              : //----------------------------------------------------------
     361              : 
     362              : // Executor only
     363              : 
     364              : /** Asynchronously launch a lazy task on the given executor.
     365              : 
     366              :     Use this to start execution of a `task<T>` that was created lazily.
     367              :     The returned wrapper must be immediately invoked with the task;
     368              :     storing the wrapper and calling it later violates LIFO ordering.
     369              : 
     370              :     With no handlers, the result is discarded and exceptions are rethrown.
     371              : 
     372              :     @par Thread Safety
     373              :     The wrapper and handlers may be called from any thread where the
     374              :     executor schedules work.
     375              : 
     376              :     @par Example
     377              :     @code
     378              :     run_async(ioc.get_executor())(my_task());
     379              :     @endcode
     380              : 
     381              :     @param ex The executor to execute the task on.
     382              : 
     383              :     @return A wrapper that accepts a `task<T>` for immediate execution.
     384              : 
     385              :     @see task
     386              :     @see executor
     387              : */
     388              : template<Executor Ex>
     389              : [[nodiscard]] auto
     390            2 : run_async(Ex ex)
     391              : {
     392              :     return run_async_wrapper<Ex, default_handler>(
     393            2 :         std::move(ex),
     394            4 :         std::stop_token{},
     395            4 :         default_handler{});
     396              : }
     397              : 
     398              : /** Asynchronously launch a lazy task with a result handler.
     399              : 
     400              :     The handler `h1` is called with the task's result on success. If `h1`
     401              :     is also invocable with `std::exception_ptr`, it handles exceptions too.
     402              :     Otherwise, exceptions are rethrown.
     403              : 
     404              :     @par Thread Safety
     405              :     The handler may be called from any thread where the executor
     406              :     schedules work.
     407              : 
     408              :     @par Example
     409              :     @code
     410              :     // Handler for result only (exceptions rethrown)
     411              :     run_async(ex, [](int result) {
     412              :         std::cout << "Got: " << result << "\n";
     413              :     })(compute_value());
     414              : 
     415              :     // Overloaded handler for both result and exception
     416              :     run_async(ex, overloaded{
     417              :         [](int result) { std::cout << "Got: " << result << "\n"; },
     418              :         [](std::exception_ptr) { std::cout << "Failed\n"; }
     419              :     })(compute_value());
     420              :     @endcode
     421              : 
     422              :     @param ex The executor to execute the task on.
     423              :     @param h1 The handler to invoke with the result (and optionally exception).
     424              : 
     425              :     @return A wrapper that accepts a `task<T>` for immediate execution.
     426              : 
     427              :     @see task
     428              :     @see executor
     429              : */
     430              : template<Executor Ex, class H1>
     431              : [[nodiscard]] auto
     432           15 : run_async(Ex ex, H1 h1)
     433              : {
     434              :     return run_async_wrapper<Ex, handler_pair<H1, default_handler>>(
     435           15 :         std::move(ex),
     436           15 :         std::stop_token{},
     437           45 :         handler_pair<H1, default_handler>{std::move(h1)});
     438              : }
     439              : 
     440              : /** Asynchronously launch a lazy task with separate result and error handlers.
     441              : 
     442              :     The handler `h1` is called with the task's result on success.
     443              :     The handler `h2` is called with the exception_ptr on failure.
     444              : 
     445              :     @par Thread Safety
     446              :     The handlers may be called from any thread where the executor
     447              :     schedules work.
     448              : 
     449              :     @par Example
     450              :     @code
     451              :     run_async(ex,
     452              :         [](int result) { std::cout << "Got: " << result << "\n"; },
     453              :         [](std::exception_ptr ep) {
     454              :             try { std::rethrow_exception(ep); }
     455              :             catch (std::exception const& e) {
     456              :                 std::cout << "Error: " << e.what() << "\n";
     457              :             }
     458              :         }
     459              :     )(compute_value());
     460              :     @endcode
     461              : 
     462              :     @param ex The executor to execute the task on.
     463              :     @param h1 The handler to invoke with the result on success.
     464              :     @param h2 The handler to invoke with the exception on failure.
     465              : 
     466              :     @return A wrapper that accepts a `task<T>` for immediate execution.
     467              : 
     468              :     @see task
     469              :     @see executor
     470              : */
     471              : template<Executor Ex, class H1, class H2>
     472              : [[nodiscard]] auto
     473           38 : run_async(Ex ex, H1 h1, H2 h2)
     474              : {
     475              :     return run_async_wrapper<Ex, handler_pair<H1, H2>>(
     476           38 :         std::move(ex),
     477           38 :         std::stop_token{},
     478          114 :         handler_pair<H1, H2>{std::move(h1), std::move(h2)});
     479              : }
     480              : 
     481              : // Ex + stop_token
     482              : 
     483              : /** Asynchronously launch a lazy task with stop token support.
     484              : 
     485              :     The stop token is propagated to the task, enabling cooperative
     486              :     cancellation. With no handlers, the result is discarded and
     487              :     exceptions are rethrown.
     488              : 
     489              :     @par Thread Safety
     490              :     The wrapper may be called from any thread where the executor
     491              :     schedules work.
     492              : 
     493              :     @par Example
     494              :     @code
     495              :     std::stop_source source;
     496              :     run_async(ex, source.get_token())(cancellable_task());
     497              :     // Later: source.request_stop();
     498              :     @endcode
     499              : 
     500              :     @param ex The executor to execute the task on.
     501              :     @param st The stop token for cooperative cancellation.
     502              : 
     503              :     @return A wrapper that accepts a `task<T>` for immediate execution.
     504              : 
     505              :     @see task
     506              :     @see executor
     507              : */
     508              : template<Executor Ex>
     509              : [[nodiscard]] auto
     510              : run_async(Ex ex, std::stop_token st)
     511              : {
     512              :     return run_async_wrapper<Ex, default_handler>(
     513              :         std::move(ex),
     514              :         std::move(st),
     515              :         default_handler{});
     516              : }
     517              : 
     518              : /** Asynchronously launch a lazy task with stop token and result handler.
     519              : 
     520              :     The stop token is propagated to the task for cooperative cancellation.
     521              :     The handler `h1` is called with the result on success, and optionally
     522              :     with exception_ptr if it accepts that type.
     523              : 
     524              :     @param ex The executor to execute the task on.
     525              :     @param st The stop token for cooperative cancellation.
     526              :     @param h1 The handler to invoke with the result (and optionally exception).
     527              : 
     528              :     @return A wrapper that accepts a `task<T>` for immediate execution.
     529              : 
     530              :     @see task
     531              :     @see executor
     532              : */
     533              : template<Executor Ex, class H1>
     534              : [[nodiscard]] auto
     535            3 : run_async(Ex ex, std::stop_token st, H1 h1)
     536              : {
     537              :     return run_async_wrapper<Ex, handler_pair<H1, default_handler>>(
     538            3 :         std::move(ex),
     539            3 :         std::move(st),
     540            6 :         handler_pair<H1, default_handler>{std::move(h1)});
     541              : }
     542              : 
     543              : /** Asynchronously launch a lazy task with stop token and separate handlers.
     544              : 
     545              :     The stop token is propagated to the task for cooperative cancellation.
     546              :     The handler `h1` is called on success, `h2` on failure.
     547              : 
     548              :     @param ex The executor to execute the task on.
     549              :     @param st The stop token for cooperative cancellation.
     550              :     @param h1 The handler to invoke with the result on success.
     551              :     @param h2 The handler to invoke with the exception on failure.
     552              : 
     553              :     @return A wrapper that accepts a `task<T>` for immediate execution.
     554              : 
     555              :     @see task
     556              :     @see executor
     557              : */
     558              : template<Executor Ex, class H1, class H2>
     559              : [[nodiscard]] auto
     560              : run_async(Ex ex, std::stop_token st, H1 h1, H2 h2)
     561              : {
     562              :     return run_async_wrapper<Ex, handler_pair<H1, H2>>(
     563              :         std::move(ex),
     564              :         std::move(st),
     565              :         handler_pair<H1, H2>{std::move(h1), std::move(h2)});
     566              : }
     567              : 
     568              : // Executor + stop_token + allocator
     569              : 
     570              : /** Asynchronously launch a lazy task with stop token and allocator.
     571              : 
     572              :     The stop token is propagated to the task for cooperative cancellation.
     573              :     The allocator parameter is reserved for future use and currently ignored.
     574              : 
     575              :     @param ex The executor to execute the task on.
     576              :     @param st The stop token for cooperative cancellation.
     577              :     @param alloc The frame allocator (currently ignored).
     578              : 
     579              :     @return A wrapper that accepts a `task<T>` for immediate execution.
     580              : 
     581              :     @see task
     582              :     @see executor
     583              :     @see frame_allocator
     584              : */
     585              : template<Executor Ex, FrameAllocator FA>
     586              : [[nodiscard]] auto
     587              : run_async(Ex ex, std::stop_token st, FA alloc)
     588              : {
     589              :     (void)alloc; // Currently ignored
     590              :     return run_async_wrapper<Ex, default_handler>(
     591              :         std::move(ex),
     592              :         std::move(st),
     593              :         default_handler{});
     594              : }
     595              : 
     596              : /** Asynchronously launch a lazy task with stop token, allocator, and handler.
     597              : 
     598              :     The stop token is propagated to the task for cooperative cancellation.
     599              :     The allocator parameter is reserved for future use and currently ignored.
     600              : 
     601              :     @param ex The executor to execute the task on.
     602              :     @param st The stop token for cooperative cancellation.
     603              :     @param alloc The frame allocator (currently ignored).
     604              :     @param h1 The handler to invoke with the result (and optionally exception).
     605              : 
     606              :     @return A wrapper that accepts a `task<T>` for immediate execution.
     607              : 
     608              :     @see task
     609              :     @see executor
     610              :     @see frame_allocator
     611              : */
     612              : template<Executor Ex, FrameAllocator FA, class H1>
     613              : [[nodiscard]] auto
     614              : run_async(Ex ex, std::stop_token st, FA alloc, H1 h1)
     615              : {
     616              :     (void)alloc; // Currently ignored
     617              :     return run_async_wrapper<Ex, handler_pair<H1, default_handler>>(
     618              :         std::move(ex),
     619              :         std::move(st),
     620              :         handler_pair<H1, default_handler>{std::move(h1)});
     621              : }
     622              : 
     623              : /** Asynchronously launch a lazy task with stop token, allocator, and handlers.
     624              : 
     625              :     The stop token is propagated to the task for cooperative cancellation.
     626              :     The allocator parameter is reserved for future use and currently ignored.
     627              : 
     628              :     @param ex The executor to execute the task on.
     629              :     @param st The stop token for cooperative cancellation.
     630              :     @param alloc The frame allocator (currently ignored).
     631              :     @param h1 The handler to invoke with the result on success.
     632              :     @param h2 The handler to invoke with the exception on failure.
     633              : 
     634              :     @return A wrapper that accepts a `task<T>` for immediate execution.
     635              : 
     636              :     @see task
     637              :     @see executor
     638              :     @see frame_allocator
     639              : */
     640              : template<Executor Ex, FrameAllocator FA, class H1, class H2>
     641              : [[nodiscard]] auto
     642              : run_async(Ex ex, std::stop_token st, FA alloc, H1 h1, H2 h2)
     643              : {
     644              :     (void)alloc; // Currently ignored
     645              :     return run_async_wrapper<Ex, handler_pair<H1, H2>>(
     646              :         std::move(ex),
     647              :         std::move(st),
     648              :         handler_pair<H1, H2>{std::move(h1), std::move(h2)});
     649              : }
     650              : 
     651              : } // namespace capy
     652              : } // namespace boost
     653              : 
     654              : #endif
        

Generated by: LCOV version 2.3