GCC Code Coverage Report


Directory: ./
File: libs/capy/include/boost/capy/ex/async_run.hpp
Date: 2026-01-15 20:40:20
Exec Total Coverage
Lines: 90 97 92.8%
Functions: 636 722 88.1%
Branches: 11 14 78.6%

Line Branch Exec Source
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_ASYNC_RUN_HPP
11 #define BOOST_CAPY_ASYNC_RUN_HPP
12
13 #include <boost/capy/detail/config.hpp>
14 #include <boost/capy/concept/affine_awaitable.hpp>
15 #include <boost/capy/ex/detail/recycling_frame_allocator.hpp>
16 #include <boost/capy/ex/frame_allocator.hpp>
17 #include <boost/capy/ex/make_affine.hpp>
18 #include <boost/capy/task.hpp>
19
20 #include <exception>
21 #include <optional>
22 #include <utility>
23
24 namespace boost {
25 namespace capy {
26
27 namespace detail {
28
29 // Discards the result on success, rethrows on exception.
30 struct default_handler
31 {
32 template<typename T>
33 void operator()(T&&) const noexcept
34 {
35 }
36
37 1 void operator()() const noexcept
38 {
39 1 }
40
41 void operator()(std::exception_ptr ep) const
42 {
43 if(ep)
44 std::rethrow_exception(ep);
45 }
46 };
47
48 // Combines two handlers into one: h1 for success, h2 for exception.
49 template<typename H1, typename H2>
50 struct handler_pair
51 {
52 H1 h1_;
53 H2 h2_;
54
55 template<typename T>
56 52 void operator()(T&& v)
57 {
58
1/1
✓ Branch 3 taken 8 times.
52 h1_(std::forward<T>(v));
59 52 }
60
61 11 void operator()()
62 {
63 11 h1_();
64 11 }
65
66 20 void operator()(std::exception_ptr ep)
67 {
68
1/1
✓ Branch 2 taken 8 times.
20 h2_(ep);
69 20 }
70 };
71
72 template<typename T>
73 struct async_run_task_result
74 {
75 std::optional<T> result_;
76
77 template<typename V>
78 39 void return_value(V&& value)
79 {
80 39 result_ = std::forward<V>(value);
81 39 }
82 };
83
84 template<>
85 struct async_run_task_result<void>
86 {
87 7 void return_void()
88 {
89 7 }
90 };
91
92 // Lifetime storage for the Dispatcher value.
93 // The Allocator is embedded in the user's coroutine frame.
94 template<
95 dispatcher Dispatcher,
96 typename T,
97 typename Handler>
98 struct async_run_task
99 {
100 struct promise_type
101 : frame_allocating_base
102 , async_run_task_result<T>
103 {
104 Dispatcher d_;
105 Handler handler_;
106 std::exception_ptr ep_;
107
108 template<typename D, typename H, typename... Args>
109 88 promise_type(D&& d, H&& h, Args&&...)
110 88 : d_(std::forward<D>(d))
111 160 , handler_(std::forward<H>(h))
112 {
113 88 }
114
115 88 async_run_task get_return_object()
116 {
117 88 return {std::coroutine_handle<promise_type>::from_promise(*this)};
118 }
119
120 /** Suspend initially.
121
122 The frame allocator is already set in TLS by the
123 embedding_frame_allocator when the user's task was created.
124 No action needed here.
125 */
126 88 std::suspend_always initial_suspend() noexcept
127 {
128 88 return {};
129 }
130
131 88 auto final_suspend() noexcept
132 {
133 struct awaiter
134 {
135 promise_type* p_;
136
137 44 bool await_ready() const noexcept
138 {
139 44 return false;
140 }
141
142 // GCC gives false positive -Wmaybe-uninitialized warnings on result_.
143 // The coroutine guarantees return_value() is called before final_suspend(),
144 // so result_ is always initialized here, but GCC's flow analysis can't prove it.
145 // GCC-12+ respects the narrow pragma scope; GCC-11 requires file-level suppression.
146 44 any_coro await_suspend(any_coro h) const noexcept
147 {
148 // Save before destroy
149 44 auto handler = std::move(p_->handler_);
150 44 auto ep = p_->ep_;
151
152 // Clear thread-local before destroy to avoid dangling pointer
153 44 frame_allocating_base::clear_frame_allocator();
154
155 // For non-void, we need to get the result before destroy
156 if constexpr (!std::is_void_v<T>)
157 {
158 #if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 12
159 #pragma GCC diagnostic push
160 #pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
161 #endif
162 36 auto result = std::move(p_->result_);
163 36 h.destroy();
164 36 if(ep)
165 9 handler(ep);
166 else
167 27 handler(std::move(*result));
168 #if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 12
169 #pragma GCC diagnostic pop
170 #endif
171 2 }
172 else
173 {
174 8 h.destroy();
175 8 if(ep)
176 1 handler(ep);
177 else
178 7 handler();
179 }
180 44 return std::noop_coroutine();
181 44 }
182
183 void await_resume() const noexcept
184 {
185 }
186 };
187 88 return awaiter{this};
188 }
189
190 20 void unhandled_exception()
191 {
192 20 ep_ = std::current_exception();
193 20 }
194
195 template<class Awaitable>
196 struct transform_awaiter
197 {
198 std::decay_t<Awaitable> a_;
199 promise_type* p_;
200
201 88 bool await_ready()
202 {
203 88 return a_.await_ready();
204 }
205
206 88 auto await_resume()
207 {
208 88 return a_.await_resume();
209 }
210
211 template<class Promise>
212 88 auto await_suspend(std::coroutine_handle<Promise> h)
213 {
214 88 return a_.await_suspend(h, p_->d_);
215 }
216 };
217
218 template<class Awaitable>
219 88 auto await_transform(Awaitable&& a)
220 {
221 using A = std::decay_t<Awaitable>;
222 if constexpr (affine_awaitable<A, Dispatcher>)
223 {
224 // Zero-overhead path for affine awaitables
225 return transform_awaiter<Awaitable>{
226 176 std::forward<Awaitable>(a), this};
227 }
228 else
229 {
230 // Trampoline fallback for legacy awaitables
231 return make_affine(std::forward<Awaitable>(a), d_);
232 }
233 88 }
234 };
235
236 std::coroutine_handle<promise_type> h_;
237
238 88 void release()
239 {
240 88 h_ = nullptr;
241 88 }
242
243 88 ~async_run_task()
244 {
245
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 44 times.
88 if(h_)
246 h_.destroy();
247 88 }
248 };
249
250 template<
251 dispatcher Dispatcher,
252 typename T,
253 typename Handler>
254 async_run_task<Dispatcher, T, Handler>
255
1/1
✓ Branch 1 taken 44 times.
88 make_async_run_task(Dispatcher, Handler, task<T> t)
256 {
257 if constexpr (std::is_void_v<T>)
258 co_await std::move(t);
259 else
260 co_return co_await std::move(t);
261 176 }
262
263 /** Runs the root task with the given dispatcher and handler.
264 */
265 template<
266 dispatcher Dispatcher,
267 typename T,
268 typename Handler>
269 void
270 88 run_async_run_task(Dispatcher d, task<T> t, Handler handler)
271 {
272
1/1
✓ Branch 2 taken 44 times.
176 auto root = make_async_run_task<Dispatcher, T, Handler>(
273 176 std::move(d), std::move(handler), std::move(t));
274
3/3
✓ Branch 3 taken 5 times.
✓ Branch 6 taken 5 times.
✓ Branch 4 taken 20 times.
88 root.h_.promise().d_(any_coro{root.h_}).resume();
275 88 root.release();
276 88 }
277
278 /** Runner object returned by async_run(dispatcher).
279
280 Provides operator() overloads to launch tasks with various
281 handler configurations. The dispatcher is captured and used
282 to schedule the task execution.
283
284 @par Frame Allocator Activation
285 The constructor sets the thread-local frame allocator, enabling
286 coroutine frame recycling for tasks created after construction.
287 This requires the single-expression usage pattern.
288
289 @par Required Usage Pattern
290 @code
291 // CORRECT: Single expression - allocator active when task created
292 async_run(ex)(make_task());
293 async_run(ex)(make_task(), handler);
294
295 // INCORRECT: Split pattern - allocator may be changed between lines
296 auto runner = async_run(ex); // Sets TLS
297 // ... other code may change TLS here ...
298 runner(make_task()); // Won't compile (deleted move)
299 @endcode
300
301 @par Enforcement Mechanisms
302 Multiple layers ensure correct usage:
303
304 @li <b>Deleted copy/move constructors</b> - Relies on C++17 guaranteed
305 copy elision. The runner can only exist as a prvalue constructed
306 directly at the call site. If this compiles, elision occurred.
307
308 @li <b>Rvalue-qualified operator()</b> - All operator() overloads are
309 &&-qualified, meaning they can only be called on rvalues. This
310 forces the idiom `async_run(ex)(task)` as a single expression.
311
312 @see async_run
313 */
314 template<
315 dispatcher Dispatcher,
316 frame_allocator Allocator = detail::recycling_frame_allocator>
317 struct async_run_awaitable
318 {
319 Dispatcher d_;
320 detail::embedding_frame_allocator<Allocator> embedder_;
321
322 /** Construct runner and activate frame allocator.
323
324 Sets the thread-local frame allocator to enable recycling
325 for coroutines created after this call.
326
327 @param d The dispatcher for task execution.
328 @param a The frame allocator (default: recycling_frame_allocator).
329 */
330 88 async_run_awaitable(Dispatcher d, Allocator a)
331 88 : d_(std::move(d))
332 88 , embedder_(std::move(a))
333 {
334 88 frame_allocating_base::set_frame_allocator(embedder_);
335 88 }
336
337 // Enforce C++17 guaranteed copy elision.
338 // If this compiles, elision occurred and &embedder_ is stable.
339 async_run_awaitable(async_run_awaitable const&) = delete;
340 async_run_awaitable(async_run_awaitable&&) = delete;
341 async_run_awaitable& operator=(async_run_awaitable const&) = delete;
342 async_run_awaitable& operator=(async_run_awaitable&&) = delete;
343
344 /** Launch task with default handler (fire-and-forget).
345
346 Uses default_handler which discards results and rethrows
347 exceptions.
348
349 @param t The task to execute.
350 */
351 template<typename T>
352 1 void operator()(task<T> t) &&
353 {
354 // Note: TLS now points to embedded wrapper in user's task frame,
355 // not to embedder_. This is expected behavior.
356
1/1
✓ Branch 2 taken 1 times.
2 run_async_run_task<Dispatcher, T, default_handler>(
357 2 std::move(d_), std::move(t), default_handler{});
358 1 }
359
360 /** Launch task with completion handler.
361
362 The handler is called on success with the result value (non-void)
363 or no arguments (void tasks). If the handler also provides an
364 overload for `std::exception_ptr`, it handles exceptions directly.
365 Otherwise, exceptions are automatically rethrown (default behavior).
366
367 @code
368 // Success-only handler (exceptions rethrow automatically)
369 async_run(ex)(my_task(), [](int result) {
370 std::cout << result;
371 });
372
373 // Full handler with exception support
374 async_run(ex)(my_task(), overloaded{
375 [](int result) { std::cout << result; },
376 [](std::exception_ptr) { }
377 });
378 @endcode
379
380 @param t The task to execute.
381 @param h The completion handler.
382 */
383 template<typename T, typename Handler>
384 1 void operator()(task<T> t, Handler h) &&
385 {
386 if constexpr (std::is_invocable_v<Handler, std::exception_ptr>)
387 {
388 // Handler handles exceptions itself
389
1/1
✓ Branch 2 taken 1 times.
2 run_async_run_task<Dispatcher, T, Handler>(
390 2 std::move(d_), std::move(t), std::move(h));
391 }
392 else
393 {
394 // Handler only handles success - pair with default exception handler
395 using combined = handler_pair<Handler, default_handler>;
396 run_async_run_task<Dispatcher, T, combined>(
397 std::move(d_), std::move(t),
398 combined{std::move(h), default_handler{}});
399 }
400 1 }
401
402 /** Launch task with separate success/error handlers.
403
404 @param t The task to execute.
405 @param h1 Handler called on success with the result value
406 (or no args for void tasks).
407 @param h2 Handler called on error with exception_ptr.
408 */
409 template<typename T, typename H1, typename H2>
410 84 void operator()(task<T> t, H1 h1, H2 h2) &&
411 {
412 using combined = handler_pair<H1, H2>;
413
1/1
✓ Branch 2 taken 42 times.
168 run_async_run_task<Dispatcher, T, combined>(
414 168 std::move(d_), std::move(t),
415 84 combined{std::move(h1), std::move(h2)});
416 84 }
417 };
418
419 } // namespace detail
420
421 /** Creates a runner to launch lazy tasks for detached execution.
422
423 Returns an async_run_awaitable that captures the dispatcher and provides
424 operator() overloads to launch tasks. This is analogous to Asio's
425 `co_spawn`. The task begins executing when the dispatcher schedules
426 it; if the dispatcher permits inline execution, the task runs
427 immediately until it awaits an I/O operation.
428
429 The dispatcher controls where and how the task resumes after each
430 suspension point. Tasks deal only with type-erased dispatchers
431 (`any_coro(any_coro)` signature), not typed executors. This leverages the
432 coroutine handle's natural type erasure.
433
434 @par Dispatcher Behavior
435 The dispatcher is invoked to start the task and propagated through
436 the coroutine chain via the affine awaitable protocol. When the task
437 completes, the handler runs on the same dispatcher context. If inline
438 execution is permitted, the call chain proceeds synchronously until
439 an I/O await suspends execution.
440
441 @par Usage
442 @code
443 io_context ioc;
444 auto ex = ioc.get_executor();
445
446 // Fire and forget (uses default_handler)
447 async_run(ex)(my_coroutine());
448
449 // Single overloaded handler
450 async_run(ex)(compute_value(), overload{
451 [](int result) { std::cout << "Got: " << result << "\n"; },
452 [](std::exception_ptr) { }
453 });
454
455 // Separate handlers: h1 for value, h2 for exception
456 async_run(ex)(compute_value(),
457 [](int result) { std::cout << result; },
458 [](std::exception_ptr ep) { if (ep) std::rethrow_exception(ep); }
459 );
460
461 // Donate thread to run queued work
462 ioc.run();
463 @endcode
464
465 @param d The dispatcher that schedules and resumes the task.
466
467 @return An async_run_awaitable object with operator() to launch tasks.
468
469 @see async_run_awaitable
470 @see task
471 @see dispatcher
472 */
473 template<dispatcher Dispatcher>
474 44 [[nodiscard]] auto async_run(Dispatcher d)
475 {
476 44 return detail::async_run_awaitable<Dispatcher>{std::move(d), {}};
477 }
478
479 /** Creates a runner with an explicit frame allocator.
480
481 @param d The dispatcher that schedules and resumes the task.
482 @param alloc The allocator for coroutine frame allocation.
483
484 @return An async_run_awaitable object with operator() to launch tasks.
485
486 @see async_run_awaitable
487 */
488 template<
489 dispatcher Dispatcher,
490 frame_allocator Allocator>
491 [[nodiscard]] auto async_run(Dispatcher d, Allocator alloc)
492 {
493 return detail::async_run_awaitable<
494 Dispatcher, Allocator>{std::move(d), std::move(alloc)};
495 }
496
497 } // namespace capy
498 } // namespace boost
499
500 #endif
501