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

Generated by: LCOV version 2.1