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-30 20:31:35 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_result.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);
     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) ->
     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) ->
     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              :     /** Submit work and return an awaitable result.
     363              : 
     364              :         The work function is executed asynchronously. The
     365              :         returned async_result can be awaited in a coroutine
     366              :         to obtain the result.
     367              : 
     368              :         @param f The work function to execute.
     369              : 
     370              :         @return An awaitable that produces the result of the work.
     371              :     */
     372              :     template<class F>
     373              :     auto
     374              :     submit(F&& f) ->
     375              :         async_result<std::invoke_result_t<std::decay_t<F>>>
     376              :         requires (!std::is_void_v<std::invoke_result_t<std::decay_t<F>>>);
     377              : 
     378              :     /** Submit work and return an awaitable result.
     379              : 
     380              :         The work function is executed asynchronously. The returned
     381              :         async_result can be awaited in a coroutine to wait
     382              :         for completion.
     383              : 
     384              :         @param f The work function to execute.
     385              : 
     386              :         @return An awaitable that completes when the work finishes.
     387              :     */
     388              :     template<class F>
     389              :     auto
     390              :     submit(F&& f) ->
     391              :         async_result<void>
     392              :         requires std::is_void_v<std::invoke_result_t<std::decay_t<F>>>;
     393              : #endif
     394              : };
     395              : 
     396              : //-----------------------------------------------------------------------------
     397              : 
     398              : /** Static vtable for type-erased executor operations.
     399              : */
     400              : struct executor::ops
     401              : {
     402              :     void* (*allocate)(void* obj, std::size_t size, std::size_t align);
     403              :     void (*deallocate)(void* obj, void* p, std::size_t size, std::size_t align);
     404              :     void (*submit)(void* obj, work* w);
     405              : };
     406              : 
     407              : /** Type-specific operation implementations.
     408              : 
     409              :     For each concrete type T, this provides static functions
     410              :     that cast the void* back to T* and forward via access.
     411              : */
     412              : template<class T>
     413              : struct executor::ops_for
     414              : {
     415              :     static void*
     416           53 :     allocate(void* obj, std::size_t size, std::size_t align)
     417              :     {
     418           53 :         return access::allocate(*static_cast<T*>(obj), size, align);
     419              :     }
     420              : 
     421              :     static void
     422            1 :     deallocate(void* obj, void* p, std::size_t size, std::size_t align)
     423              :     {
     424            1 :         access::deallocate(*static_cast<T*>(obj), p, size, align);
     425            1 :     }
     426              : 
     427              :     static void
     428           52 :     submit(void* obj, work* w)
     429              :     {
     430           52 :         access::submit(*static_cast<T*>(obj), w);
     431           52 :     }
     432              : 
     433              :     static constexpr ops table = {
     434              :         &allocate,
     435              :         &deallocate,
     436              :         &submit
     437              :     };
     438              : };
     439              : 
     440              : template<class T>
     441              : constexpr executor::ops executor::ops_for<T>::table;
     442              : 
     443              : //-----------------------------------------------------------------------------
     444              : 
     445              : /** Holder for value-type executors in owning mode.
     446              : 
     447              :     Stores the executor by value and provides the vtable
     448              :     implementation that forwards to the held executor.
     449              : */
     450              : template<class Exec>
     451              : struct executor::holder
     452              : {
     453              :     Exec ex;
     454              : 
     455              :     explicit
     456           11 :     holder(Exec e)
     457           11 :         : ex(std::move(e))
     458              :     {
     459           11 :     }
     460              : 
     461              :     static void*
     462            8 :     allocate(void* obj, std::size_t size, std::size_t align)
     463              :     {
     464            8 :         return access::allocate(
     465            8 :             static_cast<holder*>(obj)->ex, size, align);
     466              :     }
     467              : 
     468              :     static void
     469            0 :     deallocate(void* obj, void* p, std::size_t size, std::size_t align)
     470              :     {
     471            0 :         access::deallocate(
     472            0 :             static_cast<holder*>(obj)->ex, p, size, align);
     473            0 :     }
     474              : 
     475              :     static void
     476            8 :     submit(void* obj, work* w)
     477              :     {
     478            8 :         access::submit(
     479            8 :             static_cast<holder*>(obj)->ex, w);
     480            8 :     }
     481              : 
     482              :     static constexpr ops table = {
     483              :         &allocate,
     484              :         &deallocate,
     485              :         &submit
     486              :     };
     487              : };
     488              : 
     489              : template<class Exec>
     490              : constexpr executor::ops executor::holder<Exec>::table;
     491              : 
     492              : //-----------------------------------------------------------------------------
     493              : 
     494              : namespace detail {
     495              : 
     496              : // Null deleter for shared_ptr pointing to static storage
     497              : struct null_deleter
     498              : {
     499           30 :     void operator()(const void*) const noexcept {}
     500              : };
     501              : 
     502              : } // detail
     503              : 
     504              : template<class T, class>
     505           30 : executor::
     506              : executor(T& ctx) noexcept
     507           30 :     : ops_(
     508              :         &ops_for<typename std::decay<T>::type>::table,
     509              :         detail::null_deleter())
     510           30 :     , obj_(const_cast<void*>(static_cast<void const*>(std::addressof(ctx))))
     511              : {
     512           30 : }
     513              : 
     514              : template<class Exec>
     515              : executor
     516           11 : executor::
     517              : wrap(Exec ex0)
     518              : {
     519              :     typedef typename std::decay<Exec>::type exec_type;
     520              :     typedef holder<exec_type> holder_type;
     521              : 
     522           11 :     std::shared_ptr<holder_type> h =
     523           11 :         std::make_shared<holder_type>(std::move(ex0));
     524              : 
     525           11 :     executor ex;
     526              :     // Use aliasing constructor: share ownership with h,
     527              :     // but point to the static vtable
     528           11 :     ex.ops_ = std::shared_ptr<const ops>(h, &holder_type::table);
     529           11 :     ex.obj_ = h.get();
     530           22 :     return ex;
     531           11 : }
     532              : 
     533              : //-----------------------------------------------------------------------------
     534              : 
     535              : /** RAII factory for constructing and submitting work.
     536              : 
     537              :     This class manages the multi-phase process of:
     538              :     1. Allocating storage from the executor implementation
     539              :     2. Constructing work in-place via placement-new
     540              :     3. Submitting the work for execution
     541              : 
     542              :     If an exception occurs before commit(), the destructor
     543              :     will clean up any allocated resources.
     544              : 
     545              :     @par Exception Safety
     546              :     Strong guarantee. If any operation throws, all resources
     547              :     are properly released.
     548              : */
     549              : class executor::factory
     550              : {
     551              :     ops const* ops_;
     552              :     void* obj_;
     553              :     void* storage_;
     554              :     std::size_t size_;
     555              :     std::size_t align_;
     556              :     bool committed_;
     557              : 
     558              : public:
     559              :     /** Construct a factory bound to an executor.
     560              : 
     561              :         @param ex The executor to submit work to.
     562              :     */
     563              :     explicit
     564           61 :     factory(executor& ex) noexcept
     565           61 :         : ops_(ex.ops_.get())
     566           61 :         , obj_(ex.obj_)
     567           61 :         , storage_(nullptr)
     568           61 :         , size_(0)
     569           61 :         , align_(0)
     570           61 :         , committed_(false)
     571              :     {
     572           61 :     }
     573              : 
     574              :     /** Destructor. Releases resources if not committed.
     575              :     */
     576           61 :     ~factory()
     577              :     {
     578           61 :         if(storage_ && !committed_)
     579            1 :             ops_->deallocate(obj_, storage_, size_, align_);
     580           61 :     }
     581              : 
     582              :     factory(factory const&) = delete;
     583              :     factory& operator=(factory const&) = delete;
     584              : 
     585              :     /** Allocate storage for work of given size and alignment.
     586              : 
     587              :         @param size The size in bytes required.
     588              :         @param align The alignment required.
     589              :         @return Pointer to uninitialized storage.
     590              :     */
     591              :     void*
     592           61 :     allocate(std::size_t size, std::size_t align)
     593              :     {
     594           61 :         storage_ = ops_->allocate(obj_, size, align);
     595           61 :         size_ = size;
     596           61 :         align_ = align;
     597           61 :         return storage_;
     598              :     }
     599              : 
     600              :     /** Submit constructed work for execution.
     601              : 
     602              :         After calling commit(), the factory releases ownership
     603              :         and the destructor becomes a no-op.
     604              : 
     605              :         @param w Pointer to the constructed work object
     606              :                  (must reside in the allocated storage).
     607              :     */
     608              :     void
     609           60 :     commit(work* w)
     610              :     {
     611           60 :         committed_ = true;
     612           60 :         ops_->submit(obj_, w);
     613           60 :     }
     614              : };
     615              : 
     616              : //-----------------------------------------------------------------------------
     617              : 
     618              : template<class F>
     619              : void
     620           59 : executor::
     621              : post(F&& f)
     622              : {
     623              :     struct callable : work
     624              :     {
     625              :         typename std::decay<F>::type f_;
     626              : 
     627              :         explicit
     628           59 :         callable(F&& f)
     629           59 :             : f_(std::forward<F>(f))
     630              :         {
     631           59 :         }
     632              : 
     633              :         void
     634           59 :         invoke() override
     635              :         {
     636           59 :             f_();
     637           59 :         }
     638              :     };
     639              : 
     640           59 :     factory fac(*this);
     641           59 :     void* p = fac.allocate(sizeof(callable), alignof(callable));
     642           59 :     callable* w = ::new(p) callable(std::forward<F>(f));
     643           59 :     fac.commit(w);
     644           59 : }
     645              : 
     646              : //-----------------------------------------------------------------------------
     647              : 
     648              : template<class F, class Handler>
     649              : auto
     650            4 : executor::
     651              : submit(F&& f, Handler&& handler) ->
     652              :     typename std::enable_if<! std::is_void<typename
     653              :         detail::call_traits<typename std::decay<F>::type
     654              :             >::return_type>::value>::type
     655              : {
     656              :     using T = typename detail::call_traits<
     657              :         typename std::decay<F>::type>::return_type;
     658              :     using result_type = system::result<T, std::exception_ptr>;
     659              : 
     660              :     struct callable
     661              :     {
     662              :         typename std::decay<F>::type f;
     663              :         typename std::decay<Handler>::type handler;
     664              : 
     665            4 :         void operator()()
     666              :         {
     667              :             try
     668              :             {
     669            4 :                 handler(result_type(f()));
     670              :             }
     671            1 :             catch(...)
     672              :             {
     673            1 :                 handler(result_type(std::current_exception()));
     674              :             }
     675            4 :         }
     676              :     };
     677              : 
     678            4 :     post(callable{std::forward<F>(f), std::forward<Handler>(handler)});
     679            4 : }
     680              : 
     681              : template<class F, class Handler>
     682              : auto
     683            2 : executor::
     684              : submit(F&& f, Handler&& handler) ->
     685              :     typename std::enable_if<std::is_void<typename
     686              :     detail::call_traits<typename std::decay<F>::type
     687              :         >::return_type>::value>::type
     688              : {
     689              :     using result_type = system::result<void, std::exception_ptr>;
     690              : 
     691              :     struct callable
     692              :     {
     693              :         typename std::decay<F>::type f;
     694              :         typename std::decay<Handler>::type handler;
     695              : 
     696            2 :         void operator()()
     697              :         {
     698              :             try
     699              :             {
     700            2 :                 f();
     701            2 :                 handler(result_type());
     702              :             }
     703              :             catch(...)
     704              :             {
     705              :                 handler(result_type(std::current_exception()));
     706              :             }
     707            2 :         }
     708              :     };
     709              : 
     710            2 :     post(callable{std::forward<F>(f), std::forward<Handler>(handler)});
     711            2 : }
     712              : 
     713              : #ifdef BOOST_CAPY_HAS_CORO
     714              : 
     715              : template<class F>
     716              : auto
     717              : executor::
     718              : submit(F&& f) ->
     719              :     async_result<std::invoke_result_t<std::decay_t<F>>>
     720              :     requires (!std::is_void_v<std::invoke_result_t<std::decay_t<F>>>)
     721              : {
     722              :     using T = std::invoke_result_t<std::decay_t<F>>;
     723              : 
     724              :     return make_async_result<T>(
     725              :         [ex = *this, f = std::forward<F>(f)](auto on_done) mutable
     726              :         {
     727              :             ex.post(
     728              :                 [f = std::move(f),
     729              :                  on_done = std::move(on_done)]() mutable
     730              :                 {
     731              :                     on_done(f());
     732              :                 });
     733              :         });
     734              : }
     735              : 
     736              : template<class F>
     737              : auto
     738              : executor::
     739              : submit(F&& f) ->
     740              :     async_result<void>
     741              :     requires std::is_void_v<std::invoke_result_t<std::decay_t<F>>>
     742              : {
     743              :     return make_async_result<void>(
     744              :         [ex = *this, f = std::forward<F>(f)](auto on_done) mutable
     745              :         {
     746              :             ex.post(
     747              :                 [f = std::move(f),
     748              :                  on_done = std::move(on_done)]() mutable
     749              :                 {
     750              :                     f();
     751              :                     on_done();
     752              :                 });
     753              :         });
     754              : }
     755              : 
     756              : #endif
     757              : 
     758              : } // capy
     759              : } // boost
     760              : 
     761              : #endif
        

Generated by: LCOV version 2.1