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 18:26:31 Functions: 80.8 % 1117 902

            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           32 :     void operator()(T&& v)
      92              :     {
      93           32 :         h1_(std::forward<T>(v));
      94           32 :     }
      95              : 
      96              :     /// Invoke success handler for void result.
      97            6 :     void operator()()
      98              :     {
      99            6 :         h1_();
     100            6 :     }
     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           70 :     bool await_ready() const noexcept { return false; }
     168              : 
     169           70 :     bool await_suspend(std::coroutine_handle<Promise> h) noexcept
     170              :     {
     171           70 :         p_ = &h.promise();
     172           70 :         return false;
     173              :     }
     174              : 
     175           70 :     Promise& await_resume() const noexcept
     176              :     {
     177           70 :         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           70 :         trampoline get_return_object() noexcept
     202              :         {
     203              :             return trampoline{
     204           70 :                 std::coroutine_handle<promise_type>::from_promise(*this)};
     205              :         }
     206              : 
     207           70 :         std::suspend_always initial_suspend() noexcept
     208              :         {
     209           70 :             return {};
     210              :         }
     211              : 
     212              :         // Self-destruct after invoking handlers
     213           70 :         std::suspend_never final_suspend() noexcept
     214              :         {
     215           70 :             return {};
     216              :         }
     217              : 
     218           70 :         void return_void() noexcept
     219              :         {
     220           70 :         }
     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           70 :     static void invoke_impl(void* p, std::optional<Handlers>& h)
     233              :     {
     234           70 :         auto& promise = *static_cast<typename task<T>::promise_type*>(p);
     235           70 :         if(promise.ep_)
     236           13 :             (*h)(promise.ep_);
     237              :         else if constexpr(std::is_void_v<T>)
     238            8 :             (*h)();
     239              :         else
     240           49 :             (*h)(std::move(*promise.result_));
     241           70 :     }
     242              : };
     243              : 
     244              : /// Coroutine body for trampoline - invokes handlers then destroys task.
     245              : template<class Handlers>
     246              : trampoline<Handlers>
     247           70 : 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          140 : }
     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           70 :     run_async_wrapper(
     304              :         Ex ex,
     305              :         std::stop_token st,
     306              :         Handlers h)
     307           70 :         : tr_(detail::make_trampoline<Handlers>())
     308           70 :         , ex_(std::move(ex))
     309           70 :         , st_(std::move(st))
     310              :     {
     311              :         // Store handlers in the trampoline's promise
     312           70 :         tr_.h_.promise().handlers_.emplace(std::move(h));
     313           70 :     }
     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           70 :     void operator()(task<T> t) &&
     334              :     {
     335           70 :         auto task_h = t.release();
     336           70 :         auto& p = tr_.h_.promise();
     337              : 
     338              :         // Inject T-specific invoke function
     339           70 :         p.invoke_ = detail::trampoline<Handlers>::template invoke_impl<T>;
     340           70 :         p.task_promise_ = &task_h.promise();
     341           70 :         p.task_h_ = task_h;
     342              : 
     343              :         // Setup task's continuation to return to trampoline
     344           70 :         task_h.promise().continuation_ = tr_.h_;
     345           70 :         task_h.promise().caller_ex_ = ex_;
     346           70 :         task_h.promise().ex_ = ex_;  // Used by awaited async_ops
     347              : #if BOOST_CAPY_HAS_STOP_TOKEN
     348           70 :         task_h.promise().set_stop_token(st_);
     349              : #endif
     350              : 
     351              :         // Resume task through executor
     352              :         // The executor returns a handle for symmetric transfer;
     353              :         // from non-coroutine code we must explicitly resume it
     354           70 :         ex_.dispatch(task_h)();
     355           70 :     }
     356              : };
     357              : 
     358              : //----------------------------------------------------------
     359              : //
     360              : // run_async Overloads
     361              : //
     362              : //----------------------------------------------------------
     363              : 
     364              : // Executor only
     365              : 
     366              : /** Asynchronously launch a lazy task on the given executor.
     367              : 
     368              :     Use this to start execution of a `task<T>` that was created lazily.
     369              :     The returned wrapper must be immediately invoked with the task;
     370              :     storing the wrapper and calling it later violates LIFO ordering.
     371              : 
     372              :     With no handlers, the result is discarded and exceptions are rethrown.
     373              : 
     374              :     @par Thread Safety
     375              :     The wrapper and handlers may be called from any thread where the
     376              :     executor schedules work.
     377              : 
     378              :     @par Example
     379              :     @code
     380              :     run_async(ioc.get_executor())(my_task());
     381              :     @endcode
     382              : 
     383              :     @param ex The executor to execute the task on.
     384              : 
     385              :     @return A wrapper that accepts a `task<T>` for immediate execution.
     386              : 
     387              :     @see task
     388              :     @see executor
     389              : */
     390              : template<Executor Ex>
     391              : [[nodiscard]] auto
     392            2 : run_async(Ex ex)
     393              : {
     394              :     return run_async_wrapper<Ex, default_handler>(
     395            2 :         std::move(ex),
     396            4 :         std::stop_token{},
     397            4 :         default_handler{});
     398              : }
     399              : 
     400              : /** Asynchronously launch a lazy task with a result handler.
     401              : 
     402              :     The handler `h1` is called with the task's result on success. If `h1`
     403              :     is also invocable with `std::exception_ptr`, it handles exceptions too.
     404              :     Otherwise, exceptions are rethrown.
     405              : 
     406              :     @par Thread Safety
     407              :     The handler may be called from any thread where the executor
     408              :     schedules work.
     409              : 
     410              :     @par Example
     411              :     @code
     412              :     // Handler for result only (exceptions rethrown)
     413              :     run_async(ex, [](int result) {
     414              :         std::cout << "Got: " << result << "\n";
     415              :     })(compute_value());
     416              : 
     417              :     // Overloaded handler for both result and exception
     418              :     run_async(ex, overloaded{
     419              :         [](int result) { std::cout << "Got: " << result << "\n"; },
     420              :         [](std::exception_ptr) { std::cout << "Failed\n"; }
     421              :     })(compute_value());
     422              :     @endcode
     423              : 
     424              :     @param ex The executor to execute the task on.
     425              :     @param h1 The handler to invoke with the result (and optionally exception).
     426              : 
     427              :     @return A wrapper that accepts a `task<T>` for immediate execution.
     428              : 
     429              :     @see task
     430              :     @see executor
     431              : */
     432              : template<Executor Ex, class H1>
     433              : [[nodiscard]] auto
     434           15 : run_async(Ex ex, H1 h1)
     435              : {
     436              :     return run_async_wrapper<Ex, handler_pair<H1, default_handler>>(
     437           15 :         std::move(ex),
     438           15 :         std::stop_token{},
     439           45 :         handler_pair<H1, default_handler>{std::move(h1)});
     440              : }
     441              : 
     442              : /** Asynchronously launch a lazy task with separate result and error handlers.
     443              : 
     444              :     The handler `h1` is called with the task's result on success.
     445              :     The handler `h2` is called with the exception_ptr on failure.
     446              : 
     447              :     @par Thread Safety
     448              :     The handlers may be called from any thread where the executor
     449              :     schedules work.
     450              : 
     451              :     @par Example
     452              :     @code
     453              :     run_async(ex,
     454              :         [](int result) { std::cout << "Got: " << result << "\n"; },
     455              :         [](std::exception_ptr ep) {
     456              :             try { std::rethrow_exception(ep); }
     457              :             catch (std::exception const& e) {
     458              :                 std::cout << "Error: " << e.what() << "\n";
     459              :             }
     460              :         }
     461              :     )(compute_value());
     462              :     @endcode
     463              : 
     464              :     @param ex The executor to execute the task on.
     465              :     @param h1 The handler to invoke with the result on success.
     466              :     @param h2 The handler to invoke with the exception on failure.
     467              : 
     468              :     @return A wrapper that accepts a `task<T>` for immediate execution.
     469              : 
     470              :     @see task
     471              :     @see executor
     472              : */
     473              : template<Executor Ex, class H1, class H2>
     474              : [[nodiscard]] auto
     475           50 : run_async(Ex ex, H1 h1, H2 h2)
     476              : {
     477              :     return run_async_wrapper<Ex, handler_pair<H1, H2>>(
     478           50 :         std::move(ex),
     479           50 :         std::stop_token{},
     480          150 :         handler_pair<H1, H2>{std::move(h1), std::move(h2)});
     481              : }
     482              : 
     483              : // Ex + stop_token
     484              : 
     485              : /** Asynchronously launch a lazy task with stop token support.
     486              : 
     487              :     The stop token is propagated to the task, enabling cooperative
     488              :     cancellation. With no handlers, the result is discarded and
     489              :     exceptions are rethrown.
     490              : 
     491              :     @par Thread Safety
     492              :     The wrapper may be called from any thread where the executor
     493              :     schedules work.
     494              : 
     495              :     @par Example
     496              :     @code
     497              :     std::stop_source source;
     498              :     run_async(ex, source.get_token())(cancellable_task());
     499              :     // Later: source.request_stop();
     500              :     @endcode
     501              : 
     502              :     @param ex The executor to execute the task on.
     503              :     @param st The stop token for cooperative cancellation.
     504              : 
     505              :     @return A wrapper that accepts a `task<T>` for immediate execution.
     506              : 
     507              :     @see task
     508              :     @see executor
     509              : */
     510              : template<Executor Ex>
     511              : [[nodiscard]] auto
     512              : run_async(Ex ex, std::stop_token st)
     513              : {
     514              :     return run_async_wrapper<Ex, default_handler>(
     515              :         std::move(ex),
     516              :         std::move(st),
     517              :         default_handler{});
     518              : }
     519              : 
     520              : /** Asynchronously launch a lazy task with stop token and result handler.
     521              : 
     522              :     The stop token is propagated to the task for cooperative cancellation.
     523              :     The handler `h1` is called with the result on success, and optionally
     524              :     with exception_ptr if it accepts that type.
     525              : 
     526              :     @param ex The executor to execute the task on.
     527              :     @param st The stop token for cooperative cancellation.
     528              :     @param h1 The handler to invoke with the result (and optionally exception).
     529              : 
     530              :     @return A wrapper that accepts a `task<T>` for immediate execution.
     531              : 
     532              :     @see task
     533              :     @see executor
     534              : */
     535              : template<Executor Ex, class H1>
     536              : [[nodiscard]] auto
     537            3 : run_async(Ex ex, std::stop_token st, H1 h1)
     538              : {
     539              :     return run_async_wrapper<Ex, handler_pair<H1, default_handler>>(
     540            3 :         std::move(ex),
     541            3 :         std::move(st),
     542            6 :         handler_pair<H1, default_handler>{std::move(h1)});
     543              : }
     544              : 
     545              : /** Asynchronously launch a lazy task with stop token and separate handlers.
     546              : 
     547              :     The stop token is propagated to the task for cooperative cancellation.
     548              :     The handler `h1` is called on success, `h2` on failure.
     549              : 
     550              :     @param ex The executor to execute the task on.
     551              :     @param st The stop token for cooperative cancellation.
     552              :     @param h1 The handler to invoke with the result on success.
     553              :     @param h2 The handler to invoke with the exception on failure.
     554              : 
     555              :     @return A wrapper that accepts a `task<T>` for immediate execution.
     556              : 
     557              :     @see task
     558              :     @see executor
     559              : */
     560              : template<Executor Ex, class H1, class H2>
     561              : [[nodiscard]] auto
     562              : run_async(Ex ex, std::stop_token st, H1 h1, H2 h2)
     563              : {
     564              :     return run_async_wrapper<Ex, handler_pair<H1, H2>>(
     565              :         std::move(ex),
     566              :         std::move(st),
     567              :         handler_pair<H1, H2>{std::move(h1), std::move(h2)});
     568              : }
     569              : 
     570              : // Executor + stop_token + allocator
     571              : 
     572              : /** Asynchronously launch a lazy task with stop token and allocator.
     573              : 
     574              :     The stop token is propagated to the task for cooperative cancellation.
     575              :     The allocator parameter is reserved for future use and currently ignored.
     576              : 
     577              :     @param ex The executor to execute the task on.
     578              :     @param st The stop token for cooperative cancellation.
     579              :     @param alloc The frame allocator (currently ignored).
     580              : 
     581              :     @return A wrapper that accepts a `task<T>` for immediate execution.
     582              : 
     583              :     @see task
     584              :     @see executor
     585              :     @see frame_allocator
     586              : */
     587              : template<Executor Ex, FrameAllocator FA>
     588              : [[nodiscard]] auto
     589              : run_async(Ex ex, std::stop_token st, FA alloc)
     590              : {
     591              :     (void)alloc; // Currently ignored
     592              :     return run_async_wrapper<Ex, default_handler>(
     593              :         std::move(ex),
     594              :         std::move(st),
     595              :         default_handler{});
     596              : }
     597              : 
     598              : /** Asynchronously launch a lazy task with stop token, allocator, and handler.
     599              : 
     600              :     The stop token is propagated to the task for cooperative cancellation.
     601              :     The allocator parameter is reserved for future use and currently ignored.
     602              : 
     603              :     @param ex The executor to execute the task on.
     604              :     @param st The stop token for cooperative cancellation.
     605              :     @param alloc The frame allocator (currently ignored).
     606              :     @param h1 The handler to invoke with the result (and optionally exception).
     607              : 
     608              :     @return A wrapper that accepts a `task<T>` for immediate execution.
     609              : 
     610              :     @see task
     611              :     @see executor
     612              :     @see frame_allocator
     613              : */
     614              : template<Executor Ex, FrameAllocator FA, class H1>
     615              : [[nodiscard]] auto
     616              : run_async(Ex ex, std::stop_token st, FA alloc, H1 h1)
     617              : {
     618              :     (void)alloc; // Currently ignored
     619              :     return run_async_wrapper<Ex, handler_pair<H1, default_handler>>(
     620              :         std::move(ex),
     621              :         std::move(st),
     622              :         handler_pair<H1, default_handler>{std::move(h1)});
     623              : }
     624              : 
     625              : /** Asynchronously launch a lazy task with stop token, allocator, and handlers.
     626              : 
     627              :     The stop token is propagated to the task for cooperative cancellation.
     628              :     The allocator parameter is reserved for future use and currently ignored.
     629              : 
     630              :     @param ex The executor to execute the task on.
     631              :     @param st The stop token for cooperative cancellation.
     632              :     @param alloc The frame allocator (currently ignored).
     633              :     @param h1 The handler to invoke with the result on success.
     634              :     @param h2 The handler to invoke with the exception on failure.
     635              : 
     636              :     @return A wrapper that accepts a `task<T>` for immediate execution.
     637              : 
     638              :     @see task
     639              :     @see executor
     640              :     @see frame_allocator
     641              : */
     642              : template<Executor Ex, FrameAllocator FA, class H1, class H2>
     643              : [[nodiscard]] auto
     644              : run_async(Ex ex, std::stop_token st, FA alloc, H1 h1, H2 h2)
     645              : {
     646              :     (void)alloc; // Currently ignored
     647              :     return run_async_wrapper<Ex, handler_pair<H1, H2>>(
     648              :         std::move(ex),
     649              :         std::move(st),
     650              :         handler_pair<H1, H2>{std::move(h1), std::move(h2)});
     651              : }
     652              : 
     653              : } // namespace capy
     654              : } // namespace boost
     655              : 
     656              : #endif
        

Generated by: LCOV version 2.3