Lightweight 0.20261105.1
Loading...
Searching...
No Matches
Task.hpp
1// SPDX-License-Identifier: Apache-2.0
2#pragma once
3
4#include <coroutine>
5#include <exception>
6#include <utility>
7#include <variant>
8
9namespace Lightweight::Async
10{
11
12template <typename T = void>
13class Task;
14
15namespace detail
16{
17
18#if defined(__GNUC__) && !defined(__clang__)
19 /// Per-thread state of the symmetric-transfer trampoline (see @ref SymmetricTransfer).
20 struct SymmetricTransferState
21 {
22 /// The coroutine the trampoline loop on this thread is currently resuming.
23 std::coroutine_handle<> running {};
24 /// The coroutine the running one handed control to, picked up by the loop next.
25 std::coroutine_handle<> next {};
26 };
27
28 /// @return This thread's symmetric-transfer trampoline state.
29 [[nodiscard]] inline SymmetricTransferState& ThreadSymmetricTransferState() noexcept
30 {
31 thread_local SymmetricTransferState state;
32 return state;
33 }
34#endif
35
36 /// Hands control from the suspending coroutine @p from to @p to without growing the stack.
37 ///
38 /// Clang and MSVC resume a handle returned from @c await_suspend without a nested call.
39 /// GCC only does so when sibling-call optimization happens to apply (not at -O0, -Og or -O1,
40 /// nor under -fsanitize=address), so there every transfer runs through a per-thread trampoline
41 /// loop instead: a transfer made by the coroutine the loop is resuming is queued for the loop,
42 /// any other one starts a loop.
43 ///
44 /// The loop resumes coroutines from a noexcept function. Under GCC, a coroutine of another
45 /// library whose promise's @c unhandled_exception rethrows, and that awaits an @c Async::Task,
46 /// therefore ends in @c std::terminate where Clang and MSVC propagate the exception.
47 ///
48 /// @param from The coroutine that is suspending and calls this from its @c await_suspend.
49 /// @param to The coroutine to run next.
50 /// @return The handle for @c await_suspend to return.
51 [[nodiscard]] inline std::coroutine_handle<> SymmetricTransfer([[maybe_unused]] std::coroutine_handle<> from,
52 std::coroutine_handle<> to) noexcept
53 {
54#if defined(__GNUC__) && !defined(__clang__)
55 auto& state = ThreadSymmetricTransferState();
56 if (state.running == from)
57 {
58 state.next = to;
59 return std::noop_coroutine();
60 }
61
62 auto const enclosingState = state;
63 state.next = to;
64 while (state.next)
65 {
66 state.running = std::exchange(state.next, {});
67 state.running.resume();
68 }
69 state = enclosingState;
70 return std::noop_coroutine();
71#else
72 return to;
73#endif
74 }
75
76 /// Awaiter used at a Task's final suspension point.
77 ///
78 /// On completion it performs a symmetric transfer back to the awaiting coroutine
79 /// (the continuation), or to @c std::noop_coroutine() when the Task was launched
80 /// without a continuation (e.g. a detached/root task).
81 struct TaskFinalAwaiter
82 {
83 [[nodiscard]] bool await_ready() const noexcept
84 {
85 return false;
86 }
87
88 template <typename Promise>
89 [[nodiscard]] std::coroutine_handle<> await_suspend(std::coroutine_handle<Promise> coro) const noexcept
90 {
91 return SymmetricTransfer(coro, coro.promise().Continuation());
92 }
93
94 void await_resume() const noexcept {}
95 };
96
97 /// Shared promise machinery for @ref Task independent of the result type.
98 class TaskPromiseBase
99 {
100 public:
101 /// Tasks are lazy: the body does not run until the Task is awaited (or driven by SyncWait).
102 [[nodiscard]] std::suspend_always initial_suspend() noexcept
103 {
104 return {};
105 }
106
107 [[nodiscard]] TaskFinalAwaiter final_suspend() noexcept
108 {
109 return {};
110 }
111
112 /// Records the coroutine to resume once this Task completes.
113 void SetContinuation(std::coroutine_handle<> continuation) noexcept
114 {
115 _continuation = continuation;
116 }
117
118 /// Returns the continuation to symmetric-transfer to, or a no-op handle if none.
119 [[nodiscard]] std::coroutine_handle<> Continuation() const noexcept
120 {
121 return _continuation ? _continuation : std::noop_coroutine();
122 }
123
124 private:
125 std::coroutine_handle<> _continuation {};
126 };
127
128 /// Stores the outcome of a coroutine promise — either a produced value of type @c T or a captured
129 /// exception — and hands it back exactly once.
130 ///
131 /// Shared by @ref TaskPromise and @c SyncWaitPromise so the value/exception plumbing (the variant,
132 /// the move-vs-copy @c return_value overloads, and the rethrow-or-move @ref Take) lives in one
133 /// place rather than being duplicated across each promise type and its @c void specialization.
134 ///
135 /// @tparam T The value type produced by the coroutine (use the @c void specialization for none).
136 template <typename T>
137 class CoroutineResult
138 {
139 public:
140 /// Captures the in-flight exception (call from @c promise.unhandled_exception()).
141 void SetException() noexcept
142 {
143 _result.template emplace<2>(std::current_exception());
144 }
145
146 /// Stores the produced value (call from @c promise.return_value()).
147 void SetValue(T const& value)
148 {
149 _result.template emplace<1>(value);
150 }
151
152 /// Stores the produced value (call from @c promise.return_value()).
153 void SetValue(T&& value) noexcept(std::is_nothrow_move_constructible_v<T>)
154 {
155 _result.template emplace<1>(std::move(value));
156 }
157
158 /// Moves out the produced value, or rethrows the captured exception.
159 [[nodiscard]] T Take()
160 {
161 if (_result.index() == 2)
162 std::rethrow_exception(std::get<2>(_result));
163 return std::move(std::get<1>(_result));
164 }
165
166 private:
167 std::variant<std::monostate, T, std::exception_ptr> _result;
168 };
169
170 /// @c void specialization of @ref CoroutineResult: stores only a possible exception.
171 template <>
172 class CoroutineResult<void>
173 {
174 public:
175 /// Captures the in-flight exception (call from @c promise.unhandled_exception()).
176 void SetException() noexcept
177 {
178 _exception = std::current_exception();
179 }
180
181 /// Rethrows the captured exception, if any.
182 void Take() const
183 {
184 if (_exception)
185 std::rethrow_exception(_exception);
186 }
187
188 private:
189 std::exception_ptr _exception {};
190 };
191
192 /// Promise type for @c Task<T> with a non-void result.
193 ///
194 /// @tparam T The value type produced by the coroutine.
195 template <typename T>
196 class TaskPromise final: public TaskPromiseBase
197 {
198 public:
199 Task<T> get_return_object() noexcept;
200
201 void unhandled_exception() noexcept
202 {
203 _result.SetException();
204 }
205
206 void return_value(T const& value)
207 {
208 _result.SetValue(value);
209 }
210
211 void return_value(T&& value) noexcept(std::is_nothrow_move_constructible_v<T>)
212 {
213 _result.SetValue(std::move(value));
214 }
215
216 /// Moves out the produced value, or rethrows the captured exception.
217 [[nodiscard]] T Take()
218 {
219 return _result.Take();
220 }
221
222 private:
223 CoroutineResult<T> _result;
224 };
225
226 /// Promise specialization for @c Task<void>.
227 template <>
228 class TaskPromise<void> final: public TaskPromiseBase
229 {
230 public:
231 Task<void> get_return_object() noexcept;
232
233 void unhandled_exception() noexcept
234 {
235 _result.SetException();
236 }
237
238 void return_void() noexcept {}
239
240 /// Rethrows the captured exception, if any.
241 void Take() const
242 {
243 _result.Take();
244 }
245
246 private:
247 CoroutineResult<void> _result;
248 };
249
250} // namespace detail
251
252/// @ingroup Async
253/// A lazy, move-only C++23 coroutine task.
254///
255/// A @c Task<T> represents an asynchronous computation that yields a value of type @c T
256/// (or nothing for @c Task<void>). It is @b lazy — the coroutine body does not start
257/// executing until the Task is @c co_await-ed by another coroutine or driven to completion
258/// by @c SyncWait / @c SyncWaitPumping. Awaiting uses symmetric transfer, so chains of
259/// awaits do not grow the stack. Exceptions thrown inside the body are captured and
260/// rethrown at the awaiting site (parity with the throwing synchronous API).
261///
262/// @tparam T The value type produced by the task (defaults to @c void).
263template <typename T>
264class [[nodiscard]] Task
265{
266 public:
267 /// The coroutine promise type required by the C++ coroutine machinery.
268 using promise_type = detail::TaskPromise<T>;
269
270 /// The typed coroutine handle owned by this Task.
271 using Handle = std::coroutine_handle<promise_type>;
272
273 /// Constructs an empty Task that owns no coroutine.
274 Task() noexcept = default;
275
276 /// Adopts ownership of the coroutine identified by @p handle (used by the promise).
277 /// @param handle The coroutine handle to take ownership of.
278 explicit Task(Handle handle) noexcept:
279 _handle { handle }
280 {
281 }
282
283 /// Move-constructs from @p other, leaving it empty.
284 /// @param other The Task to move from.
285 Task(Task&& other) noexcept:
286 _handle { std::exchange(other._handle, {}) }
287 {
288 }
289
290 /// Move-assigns from @p other, destroying any currently-owned coroutine first.
291 /// @param other The Task to move from.
292 /// @return A reference to this Task.
293 Task& operator=(Task&& other) noexcept
294 {
295 if (this != &other)
296 {
297 if (_handle)
298 _handle.destroy();
299 _handle = std::exchange(other._handle, {});
300 }
301 return *this;
302 }
303
304 Task(Task const&) = delete;
305 Task& operator=(Task const&) = delete;
306
307 ~Task()
308 {
309 if (_handle)
310 _handle.destroy();
311 }
312
313 /// @return true if this Task owns a coroutine frame.
314 [[nodiscard]] bool IsValid() const noexcept
315 {
316 return static_cast<bool>(_handle);
317 }
318
319 /// @return true if the Task is empty or has run to completion.
320 [[nodiscard]] bool IsReady() const noexcept
321 {
322 return !_handle || _handle.done();
323 }
324
325 /// @return the underlying coroutine handle (used by SyncWait and executors).
326 [[nodiscard]] Handle GetHandle() const noexcept
327 {
328 return _handle;
329 }
330
331 /// Awaits this Task: suspends the awaiting coroutine, runs this Task, and yields its result
332 /// (or rethrows its exception) once it completes.
333 /// @return An awaiter for this Task.
334 auto operator co_await() && noexcept
335 {
336 struct Awaiter
337 {
338 Handle coro;
339
340 [[nodiscard]] bool await_ready() const noexcept
341 {
342 return !coro || coro.done();
343 }
344
345 [[nodiscard]] std::coroutine_handle<> await_suspend(std::coroutine_handle<> awaiting) noexcept
346 {
347 coro.promise().SetContinuation(awaiting);
348 return detail::SymmetricTransfer(awaiting, coro);
349 }
350
351 decltype(auto) await_resume()
352 {
353 return coro.promise().Take();
354 }
355 };
356 return Awaiter { _handle };
357 }
358
359 private:
360 Handle _handle {};
361};
362
363namespace detail
364{
365 template <typename T>
366 Task<T> TaskPromise<T>::get_return_object() noexcept
367 {
368 return Task<T> { std::coroutine_handle<TaskPromise<T>>::from_promise(*this) };
369 }
370
371 inline Task<void> TaskPromise<void>::get_return_object() noexcept
372 {
373 return Task<void> { std::coroutine_handle<TaskPromise<void>>::from_promise(*this) };
374 }
375} // namespace detail
376
377} // namespace Lightweight::Async
detail::TaskPromise< T > promise_type
The coroutine promise type required by the C++ coroutine machinery.
Definition Task.hpp:268
Handle GetHandle() const noexcept
Definition Task.hpp:326
bool IsReady() const noexcept
Definition Task.hpp:320
Task() noexcept=default
Constructs an empty Task that owns no coroutine.
bool IsValid() const noexcept
Definition Task.hpp:314
Task & operator=(Task &&other) noexcept
Definition Task.hpp:293
std::coroutine_handle< promise_type > Handle
The typed coroutine handle owned by this Task.
Definition Task.hpp:271
Task(Task &&other) noexcept
Definition Task.hpp:285