Lightweight 0.20260921.0
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 /// Awaiter used at a Task's final suspension point.
19 ///
20 /// On completion it performs a symmetric transfer back to the awaiting coroutine
21 /// (the continuation), or to @c std::noop_coroutine() when the Task was launched
22 /// without a continuation (e.g. a detached/root task).
23 struct TaskFinalAwaiter
24 {
25 [[nodiscard]] bool await_ready() const noexcept
26 {
27 return false;
28 }
29
30 template <typename Promise>
31 [[nodiscard]] std::coroutine_handle<> await_suspend(std::coroutine_handle<Promise> coro) const noexcept
32 {
33 return coro.promise().Continuation();
34 }
35
36 void await_resume() const noexcept {}
37 };
38
39 /// Shared promise machinery for @ref Task independent of the result type.
40 class TaskPromiseBase
41 {
42 public:
43 /// Tasks are lazy: the body does not run until the Task is awaited (or driven by SyncWait).
44 [[nodiscard]] std::suspend_always initial_suspend() noexcept
45 {
46 return {};
47 }
48
49 [[nodiscard]] TaskFinalAwaiter final_suspend() noexcept
50 {
51 return {};
52 }
53
54 /// Records the coroutine to resume once this Task completes.
55 void SetContinuation(std::coroutine_handle<> continuation) noexcept
56 {
57 _continuation = continuation;
58 }
59
60 /// Returns the continuation to symmetric-transfer to, or a no-op handle if none.
61 [[nodiscard]] std::coroutine_handle<> Continuation() const noexcept
62 {
63 return _continuation ? _continuation : std::noop_coroutine();
64 }
65
66 private:
67 std::coroutine_handle<> _continuation {};
68 };
69
70 /// Stores the outcome of a coroutine promise — either a produced value of type @c T or a captured
71 /// exception — and hands it back exactly once.
72 ///
73 /// Shared by @ref TaskPromise and @c SyncWaitPromise so the value/exception plumbing (the variant,
74 /// the move-vs-copy @c return_value overloads, and the rethrow-or-move @ref Take) lives in one
75 /// place rather than being duplicated across each promise type and its @c void specialization.
76 ///
77 /// @tparam T The value type produced by the coroutine (use the @c void specialization for none).
78 template <typename T>
79 class CoroutineResult
80 {
81 public:
82 /// Captures the in-flight exception (call from @c promise.unhandled_exception()).
83 void SetException() noexcept
84 {
85 _result.template emplace<2>(std::current_exception());
86 }
87
88 /// Stores the produced value (call from @c promise.return_value()).
89 void SetValue(T const& value)
90 {
91 _result.template emplace<1>(value);
92 }
93
94 /// Stores the produced value (call from @c promise.return_value()).
95 void SetValue(T&& value) noexcept(std::is_nothrow_move_constructible_v<T>)
96 {
97 _result.template emplace<1>(std::move(value));
98 }
99
100 /// Moves out the produced value, or rethrows the captured exception.
101 [[nodiscard]] T Take()
102 {
103 if (_result.index() == 2)
104 std::rethrow_exception(std::get<2>(_result));
105 return std::move(std::get<1>(_result));
106 }
107
108 private:
109 std::variant<std::monostate, T, std::exception_ptr> _result;
110 };
111
112 /// @c void specialization of @ref CoroutineResult: stores only a possible exception.
113 template <>
114 class CoroutineResult<void>
115 {
116 public:
117 /// Captures the in-flight exception (call from @c promise.unhandled_exception()).
118 void SetException() noexcept
119 {
120 _exception = std::current_exception();
121 }
122
123 /// Rethrows the captured exception, if any.
124 void Take() const
125 {
126 if (_exception)
127 std::rethrow_exception(_exception);
128 }
129
130 private:
131 std::exception_ptr _exception {};
132 };
133
134 /// Promise type for @c Task<T> with a non-void result.
135 ///
136 /// @tparam T The value type produced by the coroutine.
137 template <typename T>
138 class TaskPromise final: public TaskPromiseBase
139 {
140 public:
141 Task<T> get_return_object() noexcept;
142
143 void unhandled_exception() noexcept
144 {
145 _result.SetException();
146 }
147
148 void return_value(T const& value)
149 {
150 _result.SetValue(value);
151 }
152
153 void return_value(T&& value) noexcept(std::is_nothrow_move_constructible_v<T>)
154 {
155 _result.SetValue(std::move(value));
156 }
157
158 /// Moves out the produced value, or rethrows the captured exception.
159 [[nodiscard]] T Take()
160 {
161 return _result.Take();
162 }
163
164 private:
165 CoroutineResult<T> _result;
166 };
167
168 /// Promise specialization for @c Task<void>.
169 template <>
170 class TaskPromise<void> final: public TaskPromiseBase
171 {
172 public:
173 Task<void> get_return_object() noexcept;
174
175 void unhandled_exception() noexcept
176 {
177 _result.SetException();
178 }
179
180 void return_void() noexcept {}
181
182 /// Rethrows the captured exception, if any.
183 void Take() const
184 {
185 _result.Take();
186 }
187
188 private:
189 CoroutineResult<void> _result;
190 };
191
192} // namespace detail
193
194/// @ingroup Async
195/// A lazy, move-only C++23 coroutine task.
196///
197/// A @c Task<T> represents an asynchronous computation that yields a value of type @c T
198/// (or nothing for @c Task<void>). It is @b lazy — the coroutine body does not start
199/// executing until the Task is @c co_await-ed by another coroutine or driven to completion
200/// by @c SyncWait / @c SyncWaitPumping. Awaiting uses symmetric transfer, so chains of
201/// awaits do not grow the stack. Exceptions thrown inside the body are captured and
202/// rethrown at the awaiting site (parity with the throwing synchronous API).
203///
204/// @tparam T The value type produced by the task (defaults to @c void).
205template <typename T>
206class [[nodiscard]] Task
207{
208 public:
209 /// The coroutine promise type required by the C++ coroutine machinery.
210 using promise_type = detail::TaskPromise<T>;
211
212 /// The typed coroutine handle owned by this Task.
213 using Handle = std::coroutine_handle<promise_type>;
214
215 /// Constructs an empty Task that owns no coroutine.
216 Task() noexcept = default;
217
218 /// Adopts ownership of the coroutine identified by @p handle (used by the promise).
219 /// @param handle The coroutine handle to take ownership of.
220 explicit Task(Handle handle) noexcept:
221 _handle { handle }
222 {
223 }
224
225 /// Move-constructs from @p other, leaving it empty.
226 /// @param other The Task to move from.
227 Task(Task&& other) noexcept:
228 _handle { std::exchange(other._handle, {}) }
229 {
230 }
231
232 /// Move-assigns from @p other, destroying any currently-owned coroutine first.
233 /// @param other The Task to move from.
234 /// @return A reference to this Task.
235 Task& operator=(Task&& other) noexcept
236 {
237 if (this != &other)
238 {
239 if (_handle)
240 _handle.destroy();
241 _handle = std::exchange(other._handle, {});
242 }
243 return *this;
244 }
245
246 Task(Task const&) = delete;
247 Task& operator=(Task const&) = delete;
248
249 ~Task()
250 {
251 if (_handle)
252 _handle.destroy();
253 }
254
255 /// @return true if this Task owns a coroutine frame.
256 [[nodiscard]] bool IsValid() const noexcept
257 {
258 return static_cast<bool>(_handle);
259 }
260
261 /// @return true if the Task is empty or has run to completion.
262 [[nodiscard]] bool IsReady() const noexcept
263 {
264 return !_handle || _handle.done();
265 }
266
267 /// @return the underlying coroutine handle (used by SyncWait and executors).
268 [[nodiscard]] Handle GetHandle() const noexcept
269 {
270 return _handle;
271 }
272
273 /// Awaits this Task: suspends the awaiting coroutine, runs this Task, and yields its result
274 /// (or rethrows its exception) once it completes.
275 /// @return An awaiter for this Task.
276 auto operator co_await() && noexcept
277 {
278 struct Awaiter
279 {
280 Handle coro;
281
282 [[nodiscard]] bool await_ready() const noexcept
283 {
284 return !coro || coro.done();
285 }
286
287 [[nodiscard]] std::coroutine_handle<> await_suspend(std::coroutine_handle<> awaiting) noexcept
288 {
289 coro.promise().SetContinuation(awaiting);
290 return coro;
291 }
292
293 decltype(auto) await_resume()
294 {
295 return coro.promise().Take();
296 }
297 };
298 return Awaiter { _handle };
299 }
300
301 private:
302 Handle _handle {};
303};
304
305namespace detail
306{
307 template <typename T>
308 Task<T> TaskPromise<T>::get_return_object() noexcept
309 {
310 return Task<T> { std::coroutine_handle<TaskPromise<T>>::from_promise(*this) };
311 }
312
313 inline Task<void> TaskPromise<void>::get_return_object() noexcept
314 {
315 return Task<void> { std::coroutine_handle<TaskPromise<void>>::from_promise(*this) };
316 }
317} // namespace detail
318
319} // namespace Lightweight::Async
detail::TaskPromise< T > promise_type
The coroutine promise type required by the C++ coroutine machinery.
Definition Task.hpp:210
Handle GetHandle() const noexcept
Definition Task.hpp:268
bool IsReady() const noexcept
Definition Task.hpp:262
Task() noexcept=default
Constructs an empty Task that owns no coroutine.
bool IsValid() const noexcept
Definition Task.hpp:256
Task & operator=(Task &&other) noexcept
Definition Task.hpp:235
std::coroutine_handle< promise_type > Handle
The typed coroutine handle owned by this Task.
Definition Task.hpp:213
Task(Task &&other) noexcept
Definition Task.hpp:227