LCOV - code coverage report
Current view: top level - boost/capy - executor.hpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 95.9 % 98 94
Test Date: 2026-01-08 17:57:28 Functions: 93.1 % 159 148

            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_EXECUTOR_HPP
      11              : #define BOOST_CAPY_EXECUTOR_HPP
      12              : 
      13              : #include <boost/capy/detail/config.hpp>
      14              : #include <boost/capy/detail/call_traits.hpp>
      15              : #include <boost/capy/async_op.hpp>
      16              : #include <boost/system/result.hpp>
      17              : #include <cstddef>
      18              : #include <exception>
      19              : #include <memory>
      20              : #include <new>
      21              : #include <type_traits>
      22              : #include <utility>
      23              : 
      24              : namespace boost {
      25              : namespace capy {
      26              : 
      27              : #if 0
      28              : class execution_context
      29              : {
      30              : public:
      31              : private:
      32              :     void post(work* w);
      33              : };
      34              : #endif
      35              : 
      36              : /** A lightweight handle for submitting work to an execution context.
      37              : 
      38              :     This class provides a value-type interface for submitting
      39              :     work to be executed asynchronously. It supports two modes:
      40              : 
      41              :     @li **Reference mode**: Non-owning reference to an execution
      42              :         context. The caller must ensure the context outlives all
      43              :         executors that reference it. Created via the constructor.
      44              : 
      45              :     @li **Owning mode**: Shared ownership of a value-type executor.
      46              :         The executor is stored internally and its lifetime is
      47              :         managed automatically. Created via the `wrap()` factory.
      48              : 
      49              :     @par Thread Safety
      50              :     Distinct objects may be accessed concurrently. Shared objects
      51              :     require external synchronization.
      52              : 
      53              :     @par Implementing an Execution Context
      54              : 
      55              :     Both execution contexts (for reference mode) and value-type
      56              :     executors (for owning mode) must declare
      57              :     `friend struct executor::access` and provide three private
      58              :     member functions:
      59              : 
      60              :     @li `void* allocate(std::size_t size, std::size_t align)` —
      61              :         Allocate storage for a work item. May throw.
      62              : 
      63              :     @li `void deallocate(void* p, std::size_t size, std::size_t align)` —
      64              :         Free storage previously returned by allocate. Must not throw.
      65              : 
      66              :     @li `void submit(executor::work* w)` —
      67              :         Take ownership of the work item and arrange for execution.
      68              :         The context must eventually call `w->invoke()`, then
      69              :         `w->~work()`, then deallocate the storage.
      70              : 
      71              :     All three functions must be safe to call concurrently.
      72              : 
      73              :     @par Example (Reference Mode)
      74              :     @code
      75              :     class my_pool
      76              :     {
      77              :         friend struct executor::access;
      78              : 
      79              :         std::mutex mutex_;
      80              :         std::queue<executor::work*> queue_;
      81              : 
      82              :     public:
      83              :         void run_one()
      84              :         {
      85              :             executor::work* w = nullptr;
      86              :             {
      87              :                 std::lock_guard<std::mutex> lock(mutex_);
      88              :                 if(!queue_.empty())
      89              :                 {
      90              :                     w = queue_.front();
      91              :                     queue_.pop();
      92              :                 }
      93              :             }
      94              :             if(w)
      95              :             {
      96              :                 w->invoke();
      97              :                 std::size_t size = w->size;
      98              :                 std::size_t align = w->align;
      99              :                 w->~work();
     100              :                 deallocate(w, size, align);
     101              :             }
     102              :         }
     103              : 
     104              :     private:
     105              :         void* allocate(std::size_t size, std::size_t)
     106              :         {
     107              :             return std::malloc(size);
     108              :         }
     109              : 
     110              :         void deallocate(void* p, std::size_t, std::size_t)
     111              :         {
     112              :             std::free(p);
     113              :         }
     114              : 
     115              :         void submit(executor::work* w)
     116              :         {
     117              :             std::lock_guard<std::mutex> lock(mutex_);
     118              :             queue_.push(w);
     119              :         }
     120              :     };
     121              : 
     122              :     // Usage: reference mode
     123              :     my_pool pool;
     124              :     executor ex(pool);  // pool must outlive ex
     125              :     @endcode
     126              : 
     127              :     @par Example (Owning Mode)
     128              :     @code
     129              :     struct my_strand
     130              :     {
     131              :         friend struct executor::access;
     132              : 
     133              :         // ... internal state ...
     134              : 
     135              :     private:
     136              :         void* allocate(std::size_t size, std::size_t)
     137              :         {
     138              :             return std::malloc(size);
     139              :         }
     140              : 
     141              :         void deallocate(void* p, std::size_t, std::size_t)
     142              :         {
     143              :             std::free(p);
     144              :         }
     145              : 
     146              :         void submit(executor::work* w)
     147              :         {
     148              :             // ... queue and serialize work ...
     149              :         }
     150              :     };
     151              : 
     152              :     // Usage: owning mode
     153              :     executor ex = executor::from(my_strand{});  // executor owns the strand
     154              :     @endcode
     155              : */
     156              : class executor
     157              : {
     158              :     struct ops;
     159              : 
     160              :     template<class T>
     161              :     struct ops_for;
     162              : 
     163              :     template<class Exec>
     164              :     struct holder;
     165              : 
     166              :     std::shared_ptr<const ops> ops_;
     167              :     void* obj_;
     168              : 
     169              : public:
     170              :     /** Abstract base for type-erased work.
     171              : 
     172              :         Implementations derive from this to wrap callable
     173              :         objects for submission through the executor.
     174              : 
     175              :         @par Lifecycle
     176              : 
     177              :         When work is submitted via an executor:
     178              :         @li Storage is allocated via the context's allocate()
     179              :         @li A work-derived object is constructed in place
     180              :         @li Ownership transfers to the context via submit()
     181              :         @li The context calls invoke() to execute the work
     182              :         @li The context destroys and deallocates the work
     183              : 
     184              :         @note Work objects must not be copied or moved after
     185              :         construction. They are always destroyed in place.
     186              : 
     187              :         @note Execution contexts are responsible for tracking
     188              :         the size and alignment of allocated work objects for
     189              :         deallocation. A common pattern is to prepend metadata
     190              :         to the allocation.
     191              :     */
     192              :     struct BOOST_SYMBOL_VISIBLE work
     193              :     {
     194           60 :         virtual ~work() = default;
     195              :         virtual void invoke() = 0;
     196              :     };
     197              : 
     198              :     class factory;
     199              : 
     200              :     /** Accessor for execution context private members.
     201              : 
     202              :         Execution contexts should declare this as a friend to
     203              :         allow the executor machinery to call their private
     204              :         allocate, deallocate, and submit members:
     205              : 
     206              :         @code
     207              :         class my_context
     208              :         {
     209              :             friend struct executor::access;
     210              :             // ...
     211              :         private:
     212              :             void* allocate(std::size_t, std::size_t);
     213              :             void deallocate(void*, std::size_t, std::size_t);
     214              :             void submit(executor::work*);
     215              :         };
     216              :         @endcode
     217              :     */
     218              :     struct access
     219              :     {
     220              :         template<class T>
     221              :         static void*
     222           61 :         allocate(T& ctx, std::size_t size, std::size_t align)
     223              :         {
     224           61 :             return ctx.allocate(size, align);
     225              :         }
     226              : 
     227              :         template<class T>
     228              :         static void
     229            1 :         deallocate(T& ctx, void* p, std::size_t size, std::size_t align)
     230              :         {
     231            1 :             ctx.deallocate(p, size, align);
     232            1 :         }
     233              : 
     234              :         template<class T>
     235              :         static void
     236           60 :         submit(T& ctx, work* w)
     237              :         {
     238           60 :             ctx.submit(w);
     239           60 :         }
     240              :     };
     241              : 
     242              :     /** Construct an executor referencing an execution context.
     243              : 
     244              :         Creates an executor in reference mode. The executor holds
     245              :         a non-owning reference to the context.
     246              : 
     247              :         The implementation type must provide:
     248              :         - `void* allocate(std::size_t size, std::size_t align)`
     249              :         - `void deallocate(void* p, std::size_t size, std::size_t align)`
     250              :         - `void submit(executor::work* w)`
     251              : 
     252              :         @param ctx The execution context to reference.
     253              :         The context must outlive this executor and all copies.
     254              : 
     255              :         @see from
     256              :     */
     257              :     template<
     258              :         class T,
     259              :         class = typename std::enable_if<
     260              :             !std::is_same<
     261              :                 typename std::decay<T>::type,
     262              :                 executor>::value>::type>
     263              :     executor(T& ctx) noexcept;
     264              : 
     265              :     /** Constructor
     266              : 
     267              :         Default-constructed executors are empty.
     268              :     */
     269           17 :     executor() noexcept
     270           17 :         : ops_()
     271           17 :         , obj_(nullptr)
     272              :     {
     273           17 :     }
     274              : 
     275              :     /** Create an executor with shared ownership of a value-type executor.
     276              : 
     277              :         Creates an executor in owning mode. The provided executor
     278              :         is moved into shared storage and its lifetime is managed
     279              :         automatically via reference counting.
     280              : 
     281              :         The executor type must provide:
     282              :         - `void* allocate(std::size_t size, std::size_t align)`
     283              :         - `void deallocate(void* p, std::size_t size, std::size_t align)`
     284              :         - `void submit(executor::work* w)`
     285              : 
     286              :         @param ex The executor to wrap (moved).
     287              : 
     288              :         @return An executor that shares ownership of the wrapped executor.
     289              : 
     290              :         @par Example
     291              :         @code
     292              :         // Wrap a value-type executor
     293              :         executor ex = executor::wrap(my_strand{});
     294              : 
     295              :         // Copies share ownership (reference counted)
     296              :         executor exec2 = ex;  // both reference the same strand
     297              :         @endcode
     298              :     */
     299              :     template<class Exec>
     300              :     static executor
     301              :     wrap(Exec ex);
     302              : 
     303              :     /** Return true if the executor references an execution context.
     304              :     */
     305              :     explicit
     306           22 :     operator bool() const noexcept
     307              :     {
     308           22 :         return ops_ != nullptr;
     309              :     }
     310              : 
     311              :     /** Submit work for execution (fire-and-forget).
     312              : 
     313              :         This overload uses the allocation-aware factory
     314              :         mechanism, allowing the implementation to control
     315              :         memory allocation strategy.
     316              : 
     317              :         @param f The callable to execute.
     318              :     */
     319              :     template<class F>
     320              :     void
     321              :     post(F&& f) const;
     322              : 
     323              :     /** Submit work and invoke a handler on completion.
     324              : 
     325              :         The work function is executed asynchronously. When it
     326              :         completes, the handler is invoked with the result or
     327              :         any exception that was thrown.
     328              : 
     329              :         The handler must be invocable with the signature:
     330              :         @code
     331              :         void handler( system::result<T, std::exception_ptr> );
     332              :         @endcode
     333              :         where `T` is the return type of `f`.
     334              : 
     335              :         @param f The work function to execute.
     336              : 
     337              :         @param handler The completion handler invoked with
     338              :         the result or exception.
     339              :     */
     340              :     template<class F, class Handler>
     341              :     auto
     342              :     submit(F&& f, Handler&& handler) const ->
     343              :         typename std::enable_if<! std::is_void<
     344              :             typename detail::call_traits<typename
     345              :                 std::decay<F>::type>::return_type>::value>::type;
     346              : 
     347              :     /** Submit work and invoke a handler on completion.
     348              : 
     349              :         The work function is executed asynchronously. When it
     350              :         completes, the handler is invoked with success or any
     351              :         exception that was thrown.
     352              : 
     353              :         The handler must be invocable with the signature:
     354              :         @code
     355              :         void handler( system::result<void, std::exception_ptr> );
     356              :         @endcode
     357              : 
     358              :         @param f The work function to execute.
     359              : 
     360              :         @param handler The completion handler invoked with
     361              :         the result or exception.
     362              :     */
     363              :     template<class F, class Handler>
     364              :     auto
     365              :     submit(F&& f, Handler&& handler) const ->
     366              :         typename std::enable_if<std::is_void<typename
     367              :             detail::call_traits<typename std::decay<F>::type
     368              :                 >::return_type>::value>::type;
     369              : 
     370              : #ifdef BOOST_CAPY_HAS_CORO
     371              : 
     372              :     /** Submit work and return an awaitable result.
     373              : 
     374              :         The work function is executed asynchronously. The
     375              :         returned async_op can be awaited in a coroutine
     376              :         to obtain the result.
     377              : 
     378              :         @param f The work function to execute.
     379              : 
     380              :         @return An awaitable that produces the result of the work.
     381              :     */
     382              :     template<class F>
     383              :     auto
     384              :     submit(F&& f) const ->
     385              :         async_op<std::invoke_result_t<std::decay_t<F>>>
     386              :         requires (!std::is_void_v<std::invoke_result_t<std::decay_t<F>>>);
     387              : 
     388              :     /** Submit work and return an awaitable result.
     389              : 
     390              :         The work function is executed asynchronously. The returned
     391              :         async_op can be awaited in a coroutine to wait
     392              :         for completion.
     393              : 
     394              :         @param f The work function to execute.
     395              : 
     396              :         @return An awaitable that completes when the work finishes.
     397              :     */
     398              :     template<class F>
     399              :     auto
     400              :     submit(F&& f) const ->
     401              :         async_op<void>
     402              :         requires std::is_void_v<std::invoke_result_t<std::decay_t<F>>>;
     403              : 
     404              : #endif
     405              : };
     406              : 
     407              : //-----------------------------------------------------------------------------
     408              : 
     409              : /** Static vtable for type-erased executor operations.
     410              : */
     411              : struct executor::ops
     412              : {
     413              :     void* (*allocate)(void* obj, std::size_t size, std::size_t align);
     414              :     void (*deallocate)(void* obj, void* p, std::size_t size, std::size_t align);
     415              :     void (*submit)(void* obj, work* w);
     416              : };
     417              : 
     418              : /** Type-specific operation implementations.
     419              : 
     420              :     For each concrete type T, this provides static functions
     421              :     that cast the void* back to T* and forward via access.
     422              : */
     423              : template<class T>
     424              : struct executor::ops_for
     425              : {
     426              :     static void*
     427           53 :     allocate(void* obj, std::size_t size, std::size_t align)
     428              :     {
     429           53 :         return access::allocate(*static_cast<T*>(obj), size, align);
     430              :     }
     431              : 
     432              :     static void
     433            1 :     deallocate(void* obj, void* p, std::size_t size, std::size_t align)
     434              :     {
     435            1 :         access::deallocate(*static_cast<T*>(obj), p, size, align);
     436            1 :     }
     437              : 
     438              :     static void
     439           52 :     submit(void* obj, work* w)
     440              :     {
     441           52 :         access::submit(*static_cast<T*>(obj), w);
     442           52 :     }
     443              : 
     444              :     static constexpr ops table = {
     445              :         &allocate,
     446              :         &deallocate,
     447              :         &submit
     448              :     };
     449              : };
     450              : 
     451              : template<class T>
     452              : constexpr executor::ops executor::ops_for<T>::table;
     453              : 
     454              : //-----------------------------------------------------------------------------
     455              : 
     456              : /** Holder for value-type executors in owning mode.
     457              : 
     458              :     Stores the executor by value and provides the vtable
     459              :     implementation that forwards to the held executor.
     460              : */
     461              : template<class Exec>
     462              : struct executor::holder
     463              : {
     464              :     Exec ex;
     465              : 
     466              :     explicit
     467           11 :     holder(Exec e)
     468           11 :         : ex(std::move(e))
     469              :     {
     470           11 :     }
     471              : 
     472              :     static void*
     473            8 :     allocate(void* obj, std::size_t size, std::size_t align)
     474              :     {
     475            8 :         return access::allocate(
     476            8 :             static_cast<holder*>(obj)->ex, size, align);
     477              :     }
     478              : 
     479              :     static void
     480            0 :     deallocate(void* obj, void* p, std::size_t size, std::size_t align)
     481              :     {
     482            0 :         access::deallocate(
     483            0 :             static_cast<holder*>(obj)->ex, p, size, align);
     484            0 :     }
     485              : 
     486              :     static void
     487            8 :     submit(void* obj, work* w)
     488              :     {
     489            8 :         access::submit(
     490            8 :             static_cast<holder*>(obj)->ex, w);
     491            8 :     }
     492              : 
     493              :     static constexpr ops table = {
     494              :         &allocate,
     495              :         &deallocate,
     496              :         &submit
     497              :     };
     498              : };
     499              : 
     500              : template<class Exec>
     501              : constexpr executor::ops executor::holder<Exec>::table;
     502              : 
     503              : //-----------------------------------------------------------------------------
     504              : 
     505              : namespace detail {
     506              : 
     507              : // Null deleter for shared_ptr pointing to static storage
     508              : struct null_deleter
     509              : {
     510           30 :     void operator()(const void*) const noexcept {}
     511              : };
     512              : 
     513              : } // detail
     514              : 
     515              : template<class T, class>
     516           30 : executor::
     517              : executor(T& ctx) noexcept
     518           30 :     : ops_(
     519              :         &ops_for<typename std::decay<T>::type>::table,
     520              :         detail::null_deleter())
     521           30 :     , obj_(const_cast<void*>(static_cast<void const*>(std::addressof(ctx))))
     522              : {
     523           30 : }
     524              : 
     525              : template<class Exec>
     526              : executor
     527           11 : executor::
     528              : wrap(Exec ex0)
     529              : {
     530              :     typedef typename std::decay<Exec>::type exec_type;
     531              :     typedef holder<exec_type> holder_type;
     532              : 
     533           11 :     std::shared_ptr<holder_type> h =
     534           11 :         std::make_shared<holder_type>(std::move(ex0));
     535              : 
     536           11 :     executor ex;
     537              :     // Use aliasing constructor: share ownership with h,
     538              :     // but point to the static vtable
     539           11 :     ex.ops_ = std::shared_ptr<const ops>(h, &holder_type::table);
     540           11 :     ex.obj_ = h.get();
     541           22 :     return ex;
     542           11 : }
     543              : 
     544              : //-----------------------------------------------------------------------------
     545              : 
     546              : /** RAII factory for constructing and submitting work.
     547              : 
     548              :     This class manages the multi-phase process of:
     549              :     1. Allocating storage from the executor implementation
     550              :     2. Constructing work in-place via placement-new
     551              :     3. Submitting the work for execution
     552              : 
     553              :     If an exception occurs before commit(), the destructor
     554              :     will clean up any allocated resources.
     555              : 
     556              :     @par Exception Safety
     557              :     Strong guarantee. If any operation throws, all resources
     558              :     are properly released.
     559              : */
     560              : class executor::factory
     561              : {
     562              :     ops const* ops_;
     563              :     void* obj_;
     564              :     void* storage_;
     565              :     std::size_t size_;
     566              :     std::size_t align_;
     567              :     bool committed_;
     568              : 
     569              : public:
     570              :     /** Construct a factory bound to an executor.
     571              : 
     572              :         @param ex The executor to submit work to.
     573              :     */
     574              :     explicit
     575           61 :     factory(executor const& ex) noexcept
     576           61 :         : ops_(ex.ops_.get())
     577           61 :         , obj_(ex.obj_)
     578           61 :         , storage_(nullptr)
     579           61 :         , size_(0)
     580           61 :         , align_(0)
     581           61 :         , committed_(false)
     582              :     {
     583           61 :     }
     584              : 
     585              :     /** Destructor. Releases resources if not committed.
     586              :     */
     587           61 :     ~factory()
     588              :     {
     589           61 :         if(storage_ && !committed_)
     590            1 :             ops_->deallocate(obj_, storage_, size_, align_);
     591           61 :     }
     592              : 
     593              :     factory(factory const&) = delete;
     594              :     factory& operator=(factory const&) = delete;
     595              : 
     596              :     /** Allocate storage for work of given size and alignment.
     597              : 
     598              :         @param size The size in bytes required.
     599              :         @param align The alignment required.
     600              :         @return Pointer to uninitialized storage.
     601              :     */
     602              :     void*
     603           61 :     allocate(std::size_t size, std::size_t align)
     604              :     {
     605           61 :         storage_ = ops_->allocate(obj_, size, align);
     606           61 :         size_ = size;
     607           61 :         align_ = align;
     608           61 :         return storage_;
     609              :     }
     610              : 
     611              :     /** Submit constructed work for execution.
     612              : 
     613              :         After calling commit(), the factory releases ownership
     614              :         and the destructor becomes a no-op.
     615              : 
     616              :         @param w Pointer to the constructed work object
     617              :                  (must reside in the allocated storage).
     618              :     */
     619              :     void
     620           60 :     commit(work* w)
     621              :     {
     622           60 :         committed_ = true;
     623           60 :         ops_->submit(obj_, w);
     624           60 :     }
     625              : };
     626              : 
     627              : //-----------------------------------------------------------------------------
     628              : 
     629              : template<class F>
     630              : void
     631           59 : executor::
     632              : post(F&& f) const
     633              : {
     634              :     struct callable : work
     635              :     {
     636              :         typename std::decay<F>::type f_;
     637              : 
     638              :         explicit
     639           59 :         callable(F&& f)
     640           59 :             : f_(std::forward<F>(f))
     641              :         {
     642           59 :         }
     643              : 
     644              :         void
     645           59 :         invoke() override
     646              :         {
     647           59 :             f_();
     648           59 :         }
     649              :     };
     650              : 
     651           59 :     factory fac(*this);
     652           59 :     void* p = fac.allocate(sizeof(callable), alignof(callable));
     653           59 :     callable* w = ::new(p) callable(std::forward<F>(f));
     654           59 :     fac.commit(w);
     655           59 : }
     656              : 
     657              : //-----------------------------------------------------------------------------
     658              : 
     659              : template<class F, class Handler>
     660              : auto
     661            4 : executor::
     662              : submit(F&& f, Handler&& handler) const ->
     663              :     typename std::enable_if<! std::is_void<typename
     664              :         detail::call_traits<typename std::decay<F>::type
     665              :             >::return_type>::value>::type
     666              : {
     667              :     using T = typename detail::call_traits<
     668              :         typename std::decay<F>::type>::return_type;
     669              :     using result_type = system::result<T, std::exception_ptr>;
     670              : 
     671              :     struct callable
     672              :     {
     673              :         typename std::decay<F>::type f;
     674              :         typename std::decay<Handler>::type handler;
     675              : 
     676            4 :         void operator()()
     677              :         {
     678              :             try
     679              :             {
     680            4 :                 handler(result_type(f()));
     681              :             }
     682            1 :             catch(...)
     683              :             {
     684            1 :                 handler(result_type(std::current_exception()));
     685              :             }
     686            4 :         }
     687              :     };
     688              : 
     689            4 :     post(callable{std::forward<F>(f), std::forward<Handler>(handler)});
     690            4 : }
     691              : 
     692              : template<class F, class Handler>
     693              : auto
     694            2 : executor::
     695              : submit(F&& f, Handler&& handler) const ->
     696              :     typename std::enable_if<std::is_void<typename
     697              :     detail::call_traits<typename std::decay<F>::type
     698              :         >::return_type>::value>::type
     699              : {
     700              :     using result_type = system::result<void, std::exception_ptr>;
     701              : 
     702              :     struct callable
     703              :     {
     704              :         typename std::decay<F>::type f;
     705              :         typename std::decay<Handler>::type handler;
     706              : 
     707            2 :         void operator()()
     708              :         {
     709              :             try
     710              :             {
     711            2 :                 f();
     712            2 :                 handler(result_type());
     713              :             }
     714              :             catch(...)
     715              :             {
     716              :                 handler(result_type(std::current_exception()));
     717              :             }
     718            2 :         }
     719              :     };
     720              : 
     721            2 :     post(callable{std::forward<F>(f), std::forward<Handler>(handler)});
     722            2 : }
     723              : 
     724              : #ifdef BOOST_CAPY_HAS_CORO
     725              : 
     726              : template<class F>
     727              : auto
     728              : executor::
     729              : submit(F&& f) const ->
     730              :     async_op<std::invoke_result_t<std::decay_t<F>>>
     731              :     requires (!std::is_void_v<std::invoke_result_t<std::decay_t<F>>>)
     732              : {
     733              :     using T = std::invoke_result_t<std::decay_t<F>>;
     734              : 
     735              :     return make_async_op<T>(
     736              :         [ex = *this, f = std::forward<F>(f)](auto on_done) mutable
     737              :         {
     738              :             ex.post(
     739              :                 [f = std::move(f),
     740              :                  on_done = std::move(on_done)]() mutable
     741              :                 {
     742              :                     on_done(f());
     743              :                 });
     744              :         });
     745              : }
     746              : 
     747              : template<class F>
     748              : auto
     749              : executor::
     750              : submit(F&& f) const ->
     751              :     async_op<void>
     752              :     requires std::is_void_v<std::invoke_result_t<std::decay_t<F>>>
     753              : {
     754              :     return make_async_op<void>(
     755              :         [ex = *this, f = std::forward<F>(f)](auto on_done) mutable
     756              :         {
     757              :             ex.post(
     758              :                 [f = std::move(f),
     759              :                  on_done = std::move(on_done)]() mutable
     760              :                 {
     761              :                     f();
     762              :                     on_done();
     763              :                 });
     764              :         });
     765              : }
     766              : 
     767              : #endif
     768              : 
     769              : } // capy
     770              : } // boost
     771              : 
     772              : #endif
        

Generated by: LCOV version 2.1