93.81% Lines (91/97)
96.55% Functions (28/29)
| TLA | Baseline | Branch | ||||||
|---|---|---|---|---|---|---|---|---|
| Line | Hits | Code | Line | Hits | Code | |||
| 1 | + | // | ||||||
| 2 | + | // Copyright (c) 2026 Michael Vandeberg | ||||||
| 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_QUITTER_HPP | ||||||
| 11 | + | #define BOOST_CAPY_QUITTER_HPP | ||||||
| 12 | + | |||||||
| 13 | + | #include <boost/capy/detail/config.hpp> | ||||||
| 14 | + | #include <boost/capy/detail/stop_requested_exception.hpp> | ||||||
| 15 | + | #include <boost/capy/concept/executor.hpp> | ||||||
| 16 | + | #include <boost/capy/concept/io_awaitable.hpp> | ||||||
| 17 | + | #include <boost/capy/ex/io_awaitable_promise_base.hpp> | ||||||
| 18 | + | #include <boost/capy/ex/io_env.hpp> | ||||||
| 19 | + | #include <boost/capy/ex/frame_allocator.hpp> | ||||||
| 20 | + | #include <boost/capy/detail/await_suspend_helper.hpp> | ||||||
| 21 | + | |||||||
| 22 | + | #include <exception> | ||||||
| 23 | + | #include <optional> | ||||||
| 24 | + | #include <type_traits> | ||||||
| 25 | + | #include <utility> | ||||||
| 26 | + | |||||||
| 27 | + | /* Stop-aware coroutine task. | ||||||
| 28 | + | |||||||
| 29 | + | quitter<T> is identical to task<T> except that when the stop token | ||||||
| 30 | + | is triggered, the coroutine body never sees the cancellation. The | ||||||
| 31 | + | promise intercepts it on resume (in transform_awaiter::await_resume) | ||||||
| 32 | + | and throws a sentinel exception that unwinds through RAII destructors | ||||||
| 33 | + | to final_suspend. The parent sees a "stopped" completion. | ||||||
| 34 | + | |||||||
| 35 | + | See doc/quitter.md for the full design rationale. */ | ||||||
| 36 | + | |||||||
| 37 | + | namespace boost { | ||||||
| 38 | + | namespace capy { | ||||||
| 39 | + | |||||||
| 40 | + | namespace detail { | ||||||
| 41 | + | |||||||
| 42 | + | // Reuse the same return-value storage as task<T>. | ||||||
| 43 | + | // task_return_base is defined in task.hpp, but quitter needs its own | ||||||
| 44 | + | // copy to avoid a header dependency on task.hpp. | ||||||
| 45 | + | template<typename T> | ||||||
| 46 | + | struct quitter_return_base | ||||||
| 47 | + | { | ||||||
| 48 | + | std::optional<T> result_; | ||||||
| 49 | + | |||||||
| HITGNC | 50 | + | 9 | void return_value(T value) | ||||
| 51 | + | { | ||||||
| HITGNC | 52 | + | 9 | result_ = std::move(value); | ||||
| HITGNC | 53 | + | 9 | } | ||||
| 54 | + | |||||||
| HITGNC | 55 | + | 3 | T&& result() noexcept | ||||
| 56 | + | { | ||||||
| HITGNC | 57 | + | 3 | return std::move(*result_); | ||||
| 58 | + | } | ||||||
| 59 | + | }; | ||||||
| 60 | + | |||||||
| 61 | + | template<> | ||||||
| 62 | + | struct quitter_return_base<void> | ||||||
| 63 | + | { | ||||||
| HITGNC | 64 | + | 1 | void return_void() | ||||
| 65 | + | { | ||||||
| HITGNC | 66 | + | 1 | } | ||||
| 67 | + | }; | ||||||
| 68 | + | |||||||
| 69 | + | } // namespace detail | ||||||
| 70 | + | |||||||
| 71 | + | /** Stop-aware lazy coroutine task satisfying @ref IoRunnable. | ||||||
| 72 | + | |||||||
| 73 | + | When the stop token is triggered, the next `co_await` inside the | ||||||
| 74 | + | coroutine short-circuits: the body never sees the result and RAII | ||||||
| 75 | + | destructors run normally. The parent observes a "stopped" | ||||||
| 76 | + | completion via @ref promise_type::stopped. | ||||||
| 77 | + | |||||||
| 78 | + | Everything else — frame allocation, environment propagation, | ||||||
| 79 | + | symmetric transfer, move semantics — is identical to @ref task. | ||||||
| 80 | + | |||||||
| 81 | + | @tparam T The result type. Use `quitter<>` for `quitter<void>`. | ||||||
| 82 | + | |||||||
| 83 | + | @see task, IoRunnable, IoAwaitable | ||||||
| 84 | + | */ | ||||||
| 85 | + | template<typename T = void> | ||||||
| 86 | + | struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE | ||||||
| 87 | + | quitter | ||||||
| 88 | + | { | ||||||
| 89 | + | struct promise_type | ||||||
| 90 | + | : io_awaitable_promise_base<promise_type> | ||||||
| 91 | + | , detail::quitter_return_base<T> | ||||||
| 92 | + | { | ||||||
| 93 | + | private: | ||||||
| 94 | + | friend quitter; | ||||||
| 95 | + | |||||||
| 96 | + | enum class completion { running, value, exception, stopped }; | ||||||
| 97 | + | |||||||
| 98 | + | union { std::exception_ptr ep_; }; | ||||||
| 99 | + | completion state_; | ||||||
| 100 | + | |||||||
| 101 | + | public: | ||||||
| HITGNC | 102 | + | 28 | promise_type() noexcept | ||||
| HITGNC | 103 | + | 28 | : state_(completion::running) | ||||
| 104 | + | { | ||||||
| HITGNC | 105 | + | 28 | } | ||||
| 106 | + | |||||||
| HITGNC | 107 | + | 28 | ~promise_type() | ||||
| 108 | + | { | ||||||
| HITGNC | 109 | + | 28 | if(state_ == completion::exception || | ||||
| HITGNC | 110 | + | 26 | state_ == completion::stopped) | ||||
| HITGNC | 111 | + | 18 | ep_.~exception_ptr(); | ||||
| HITGNC | 112 | + | 28 | } | ||||
| 113 | + | |||||||
| 114 | + | /// Return a non-null exception_ptr when the coroutine threw | ||||||
| 115 | + | /// or was stopped. Stopped quitters report the sentinel | ||||||
| 116 | + | /// stop_requested_exception so that run_async routes to | ||||||
| 117 | + | /// the error handler instead of accessing a non-existent | ||||||
| 118 | + | /// result. | ||||||
| HITGNC | 119 | + | 22 | std::exception_ptr exception() const noexcept | ||||
| 120 | + | { | ||||||
| HITGNC | 121 | + | 22 | if(state_ == completion::exception || | ||||
| HITGNC | 122 | + | 18 | state_ == completion::stopped) | ||||
| HITGNC | 123 | + | 18 | return ep_; | ||||
| HITGNC | 124 | + | 4 | return {}; | ||||
| 125 | + | } | ||||||
| 126 | + | |||||||
| 127 | + | /// True when the coroutine was stopped via the stop token. | ||||||
| HITGNC | 128 | + | 10 | bool stopped() const noexcept | ||||
| 129 | + | { | ||||||
| HITGNC | 130 | + | 10 | return state_ == completion::stopped; | ||||
| 131 | + | } | ||||||
| 132 | + | |||||||
| HITGNC | 133 | + | 28 | quitter get_return_object() | ||||
| 134 | + | { | ||||||
| 135 | + | return quitter{ | ||||||
| HITGNC | 136 | + | 28 | std::coroutine_handle<promise_type>::from_promise(*this)}; | ||||
| 137 | + | } | ||||||
| 138 | + | |||||||
| HITGNC | 139 | + | 28 | auto initial_suspend() noexcept | ||||
| 140 | + | { | ||||||
| 141 | + | struct awaiter | ||||||
| 142 | + | { | ||||||
| 143 | + | promise_type* p_; | ||||||
| 144 | + | |||||||
| HITGNC | 145 | + | 28 | bool await_ready() const noexcept | ||||
| 146 | + | { | ||||||
| HITGNC | 147 | + | 28 | return false; | ||||
| 148 | + | } | ||||||
| 149 | + | |||||||
| HITGNC | 150 | + | 28 | void await_suspend(std::coroutine_handle<>) const noexcept | ||||
| 151 | + | { | ||||||
| HITGNC | 152 | + | 28 | } | ||||
| 153 | + | |||||||
| 154 | + | // Potentially-throwing: checks the stop token before | ||||||
| 155 | + | // the coroutine body executes its first statement. | ||||||
| HITGNC | 156 | + | 28 | void await_resume() const | ||||
| 157 | + | { | ||||||
| HITGNC | 158 | + | 28 | set_current_frame_allocator( | ||||
| HITGNC | 159 | + | 28 | p_->environment()->frame_allocator); | ||||
| HITGNC | 160 | + | 28 | if(p_->environment()->stop_token.stop_requested()) | ||||
| HITGNC | 161 | + | 3 | throw detail::stop_requested_exception{}; | ||||
| HITGNC | 162 | + | 25 | } | ||||
| 163 | + | }; | ||||||
| HITGNC | 164 | + | 28 | return awaiter{this}; | ||||
| 165 | + | } | ||||||
| 166 | + | |||||||
| HITGNC | 167 | + | 28 | auto final_suspend() noexcept | ||||
| 168 | + | { | ||||||
| 169 | + | struct awaiter | ||||||
| 170 | + | { | ||||||
| 171 | + | promise_type* p_; | ||||||
| 172 | + | |||||||
| HITGNC | 173 | + | 28 | bool await_ready() const noexcept | ||||
| 174 | + | { | ||||||
| HITGNC | 175 | + | 28 | return false; | ||||
| 176 | + | } | ||||||
| 177 | + | |||||||
| HITGNC | 178 | + | 28 | std::coroutine_handle<> await_suspend( | ||||
| 179 | + | std::coroutine_handle<>) const noexcept | ||||||
| 180 | + | { | ||||||
| HITGNC | 181 | + | 28 | return p_->continuation(); | ||||
| 182 | + | } | ||||||
| 183 | + | |||||||
| MISUNC | 184 | + | ✗ | void await_resume() const noexcept | ||||
| 185 | + | { | ||||||
| MISUNC | 186 | + | ✗ | } | ||||
| 187 | + | }; | ||||||
| HITGNC | 188 | + | 28 | return awaiter{this}; | ||||
| 189 | + | } | ||||||
| 190 | + | |||||||
| HITGNC | 191 | + | 18 | void unhandled_exception() | ||||
| 192 | + | { | ||||||
| 193 | + | try | ||||||
| 194 | + | { | ||||||
| HITGNC | 195 | + | 18 | throw; | ||||
| 196 | + | } | ||||||
| HITGNC | 197 | + | 18 | catch(detail::stop_requested_exception const&) | ||||
| 198 | + | { | ||||||
| 199 | + | // Store the exception_ptr so that run_async's | ||||||
| 200 | + | // invoke_impl routes to the error handler | ||||||
| 201 | + | // instead of accessing a non-existent result. | ||||||
| HITGNC | 202 | + | 16 | new (&ep_) std::exception_ptr( | ||||
| 203 | + | std::current_exception()); | ||||||
| HITGNC | 204 | + | 16 | state_ = completion::stopped; | ||||
| 205 | + | } | ||||||
| HITGNC | 206 | + | 2 | catch(...) | ||||
| 207 | + | { | ||||||
| HITGNC | 208 | + | 2 | new (&ep_) std::exception_ptr( | ||||
| 209 | + | std::current_exception()); | ||||||
| HITGNC | 210 | + | 2 | state_ = completion::exception; | ||||
| 211 | + | } | ||||||
| HITGNC | 212 | + | 18 | } | ||||
| 213 | + | |||||||
| 214 | + | //------------------------------------------------------ | ||||||
| 215 | + | // transform_awaitable — the key difference from task<T> | ||||||
| 216 | + | //------------------------------------------------------ | ||||||
| 217 | + | |||||||
| 218 | + | template<class Awaitable> | ||||||
| 219 | + | struct transform_awaiter | ||||||
| 220 | + | { | ||||||
| 221 | + | std::decay_t<Awaitable> a_; | ||||||
| 222 | + | promise_type* p_; | ||||||
| 223 | + | |||||||
| HITGNC | 224 | + | 18 | bool await_ready() noexcept | ||||
| 225 | + | { | ||||||
| HITGNC | 226 | + | 18 | return a_.await_ready(); | ||||
| 227 | + | } | ||||||
| 228 | + | |||||||
| 229 | + | // Check the stop token BEFORE the coroutine body | ||||||
| 230 | + | // sees the result of the I/O operation. | ||||||
| HITGNC | 231 | + | 18 | decltype(auto) await_resume() | ||||
| 232 | + | { | ||||||
| HITGNC | 233 | + | 18 | set_current_frame_allocator( | ||||
| HITGNC | 234 | + | 18 | p_->environment()->frame_allocator); | ||||
| HITGNC | 235 | + | 18 | if(p_->environment()->stop_token.stop_requested()) | ||||
| HITGNC | 236 | + | 13 | throw detail::stop_requested_exception{}; | ||||
| HITGNC | 237 | + | 5 | return a_.await_resume(); | ||||
| 238 | + | } | ||||||
| 239 | + | |||||||
| 240 | + | template<class Promise> | ||||||
| HITGNC | 241 | + | 16 | auto await_suspend( | ||||
| 242 | + | std::coroutine_handle<Promise> h) noexcept | ||||||
| 243 | + | { | ||||||
| 244 | + | using R = decltype( | ||||||
| 245 | + | a_.await_suspend(h, p_->environment())); | ||||||
| 246 | + | if constexpr (std::is_same_v< | ||||||
| 247 | + | R, std::coroutine_handle<>>) | ||||||
| HITGNC | 248 | + | 16 | return detail::symmetric_transfer( | ||||
| HITGNC | 249 | + | 32 | a_.await_suspend(h, p_->environment())); | ||||
| 250 | + | else | ||||||
| MISUNC | 251 | + | ✗ | return a_.await_suspend( | ||||
| MISUNC | 252 | + | ✗ | h, p_->environment()); | ||||
| 253 | + | } | ||||||
| 254 | + | }; | ||||||
| 255 | + | |||||||
| 256 | + | template<class Awaitable> | ||||||
| HITGNC | 257 | + | 18 | auto transform_awaitable(Awaitable&& a) | ||||
| 258 | + | { | ||||||
| 259 | + | using A = std::decay_t<Awaitable>; | ||||||
| 260 | + | if constexpr (IoAwaitable<A>) | ||||||
| 261 | + | { | ||||||
| 262 | + | return transform_awaiter<Awaitable>{ | ||||||
| HITGNC | 263 | + | 33 | std::forward<Awaitable>(a), this}; | ||||
| 264 | + | } | ||||||
| 265 | + | else | ||||||
| 266 | + | { | ||||||
| 267 | + | static_assert(sizeof(A) == 0, | ||||||
| 268 | + | "requires IoAwaitable"); | ||||||
| 269 | + | } | ||||||
| HITGNC | 270 | + | 15 | } | ||||
| 271 | + | }; | ||||||
| 272 | + | |||||||
| 273 | + | std::coroutine_handle<promise_type> h_; | ||||||
| 274 | + | |||||||
| 275 | + | /// Destroy the quitter and its coroutine frame if owned. | ||||||
| HITGNC | 276 | + | 72 | ~quitter() | ||||
| 277 | + | { | ||||||
| HITGNC | 278 | + | 72 | if(h_) | ||||
| HITGNC | 279 | + | 13 | h_.destroy(); | ||||
| HITGNC | 280 | + | 72 | } | ||||
| 281 | + | |||||||
| 282 | + | /// Return false; quitters are never immediately ready. | ||||||
| HITGNC | 283 | + | 13 | bool await_ready() const noexcept | ||||
| 284 | + | { | ||||||
| HITGNC | 285 | + | 13 | return false; | ||||
| 286 | + | } | ||||||
| 287 | + | |||||||
| 288 | + | /** Return the result, rethrow exception, or propagate stop. | ||||||
| 289 | + | |||||||
| 290 | + | When stopped, throws stop_requested_exception so that a | ||||||
| 291 | + | parent quitter also stops. A parent task<T> will see this | ||||||
| 292 | + | as an unhandled exception — by design. | ||||||
| 293 | + | */ | ||||||
| HITGNC | 294 | + | 10 | auto await_resume() | ||||
| 295 | + | { | ||||||
| HITGNC | 296 | + | 10 | if(h_.promise().stopped()) | ||||
| HITGNC | 297 | + | 6 | throw detail::stop_requested_exception{}; | ||||
| HITGNC | 298 | + | 4 | if(h_.promise().state_ == promise_type::completion::exception) | ||||
| MISUNC | 299 | + | ✗ | std::rethrow_exception(h_.promise().ep_); | ||||
| 300 | + | if constexpr (! std::is_void_v<T>) | ||||||
| HITGNC | 301 | + | 4 | return std::move(*h_.promise().result_); | ||||
| 302 | + | else | ||||||
| MISUNC | 303 | + | ✗ | return; | ||||
| 304 | + | } | ||||||
| 305 | + | |||||||
| 306 | + | /// Start execution with the caller's context. | ||||||
| HITGNC | 307 | + | 13 | std::coroutine_handle<> await_suspend( | ||||
| 308 | + | std::coroutine_handle<> cont, | ||||||
| 309 | + | io_env const* env) | ||||||
| 310 | + | { | ||||||
| HITGNC | 311 | + | 13 | h_.promise().set_continuation(cont); | ||||
| HITGNC | 312 | + | 13 | h_.promise().set_environment(env); | ||||
| HITGNC | 313 | + | 13 | return h_; | ||||
| 314 | + | } | ||||||
| 315 | + | |||||||
| 316 | + | /** Return the coroutine handle. | ||||||
| 317 | + | |||||||
| 318 | + | @note Do not call `destroy()` on the returned handle while | ||||||
| 319 | + | the quitter is being awaited. The quitter's lifetime is | ||||||
| 320 | + | normally managed by `run_async`, `run`, or the awaiting | ||||||
| 321 | + | parent; manually destroying a suspended quitter that another | ||||||
| 322 | + | coroutine is awaiting produces undefined behavior. For | ||||||
| 323 | + | cooperative cancellation, use `std::stop_token`. | ||||||
| 324 | + | |||||||
| 325 | + | @return The coroutine handle. | ||||||
| 326 | + | */ | ||||||
| HITGNC | 327 | + | 17 | std::coroutine_handle<promise_type> handle() const noexcept | ||||
| 328 | + | { | ||||||
| HITGNC | 329 | + | 17 | return h_; | ||||
| 330 | + | } | ||||||
| 331 | + | |||||||
| 332 | + | /** Release ownership of the coroutine frame. | ||||||
| 333 | + | |||||||
| 334 | + | @note If the caller intends to call `destroy()` on the | ||||||
| 335 | + | released handle, it must do so only when the quitter has not | ||||||
| 336 | + | started or has fully completed. Destroying a suspended | ||||||
| 337 | + | quitter that is being awaited produces undefined behavior. | ||||||
| 338 | + | */ | ||||||
| HITGNC | 339 | + | 15 | void release() noexcept | ||||
| 340 | + | { | ||||||
| HITGNC | 341 | + | 15 | h_ = nullptr; | ||||
| HITGNC | 342 | + | 15 | } | ||||
| 343 | + | |||||||
| 344 | + | quitter(quitter const&) = delete; | ||||||
| 345 | + | quitter& operator=(quitter const&) = delete; | ||||||
| 346 | + | |||||||
| 347 | + | /// Construct by moving, transferring ownership. | ||||||
| HITGNC | 348 | + | 44 | quitter(quitter&& other) noexcept | ||||
| HITGNC | 349 | + | 44 | : h_(std::exchange(other.h_, nullptr)) | ||||
| 350 | + | { | ||||||
| HITGNC | 351 | + | 44 | } | ||||
| 352 | + | |||||||
| 353 | + | /// Assign by moving, transferring ownership. | ||||||
| 354 | + | quitter& operator=(quitter&& other) noexcept | ||||||
| 355 | + | { | ||||||
| 356 | + | if(this != &other) | ||||||
| 357 | + | { | ||||||
| 358 | + | if(h_) | ||||||
| 359 | + | h_.destroy(); | ||||||
| 360 | + | h_ = std::exchange(other.h_, nullptr); | ||||||
| 361 | + | } | ||||||
| 362 | + | return *this; | ||||||
| 363 | + | } | ||||||
| 364 | + | |||||||
| 365 | + | private: | ||||||
| HITGNC | 366 | + | 28 | explicit quitter(std::coroutine_handle<promise_type> h) | ||||
| HITGNC | 367 | + | 28 | : h_(h) | ||||
| 368 | + | { | ||||||
| HITGNC | 369 | + | 28 | } | ||||
| 370 | + | }; | ||||||
| 371 | + | |||||||
| 372 | + | } // namespace capy | ||||||
| 373 | + | } // namespace boost | ||||||
| 374 | + | |||||||
| 375 | + | #endif | ||||||