Lightweight 0.20260617.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/// A lazy, move-only C++23 coroutine task.
195///
196/// A @c Task<T> represents an asynchronous computation that yields a value of type @c T
197/// (or nothing for @c Task<void>). It is @b lazy — the coroutine body does not start
198/// executing until the Task is @c co_await-ed by another coroutine or driven to completion
199/// by @c SyncWait / @c SyncWaitPumping. Awaiting uses symmetric transfer, so chains of
200/// awaits do not grow the stack. Exceptions thrown inside the body are captured and
201/// rethrown at the awaiting site (parity with the throwing synchronous API).
202///
203/// @tparam T The value type produced by the task (defaults to @c void).
204template <typename T>
205class [[nodiscard]] Task
206{
207 public:
208 /// The coroutine promise type required by the C++ coroutine machinery.
209 using promise_type = detail::TaskPromise<T>;
210
211 /// The typed coroutine handle owned by this Task.
212 using Handle = std::coroutine_handle<promise_type>;
213
214 /// Constructs an empty Task that owns no coroutine.
215 Task() noexcept = default;
216
217 /// Adopts ownership of the coroutine identified by @p handle (used by the promise).
218 /// @param handle The coroutine handle to take ownership of.
219 explicit Task(Handle handle) noexcept:
220 _handle { handle }
221 {
222 }
223
224 /// Move-constructs from @p other, leaving it empty.
225 /// @param other The Task to move from.
226 Task(Task&& other) noexcept:
227 _handle { std::exchange(other._handle, {}) }
228 {
229 }
230
231 /// Move-assigns from @p other, destroying any currently-owned coroutine first.
232 /// @param other The Task to move from.
233 /// @return A reference to this Task.
234 Task& operator=(Task&& other) noexcept
235 {
236 if (this != &other)
237 {
238 if (_handle)
239 _handle.destroy();
240 _handle = std::exchange(other._handle, {});
241 }
242 return *this;
243 }
244
245 Task(Task const&) = delete;
246 Task& operator=(Task const&) = delete;
247
248 ~Task()
249 {
250 if (_handle)
251 _handle.destroy();
252 }
253
254 /// @return true if this Task owns a coroutine frame.
255 [[nodiscard]] bool IsValid() const noexcept
256 {
257 return static_cast<bool>(_handle);
258 }
259
260 /// @return true if the Task is empty or has run to completion.
261 [[nodiscard]] bool IsReady() const noexcept
262 {
263 return !_handle || _handle.done();
264 }
265
266 /// @return the underlying coroutine handle (used by SyncWait and executors).
267 [[nodiscard]] Handle GetHandle() const noexcept
268 {
269 return _handle;
270 }
271
272 /// Awaits this Task: suspends the awaiting coroutine, runs this Task, and yields its result
273 /// (or rethrows its exception) once it completes.
274 /// @return An awaiter for this Task.
275 auto operator co_await() && noexcept
276 {
277 struct Awaiter
278 {
279 Handle coro;
280
281 [[nodiscard]] bool await_ready() const noexcept
282 {
283 return !coro || coro.done();
284 }
285
286 [[nodiscard]] std::coroutine_handle<> await_suspend(std::coroutine_handle<> awaiting) noexcept
287 {
288 coro.promise().SetContinuation(awaiting);
289 return coro;
290 }
291
292 decltype(auto) await_resume()
293 {
294 return coro.promise().Take();
295 }
296 };
297 return Awaiter { _handle };
298 }
299
300 private:
301 Handle _handle {};
302};
303
304namespace detail
305{
306 template <typename T>
307 Task<T> TaskPromise<T>::get_return_object() noexcept
308 {
309 return Task<T> { std::coroutine_handle<TaskPromise<T>>::from_promise(*this) };
310 }
311
312 inline Task<void> TaskPromise<void>::get_return_object() noexcept
313 {
314 return Task<void> { std::coroutine_handle<TaskPromise<void>>::from_promise(*this) };
315 }
316} // namespace detail
317
318} // namespace Lightweight::Async
detail::TaskPromise< T > promise_type
The coroutine promise type required by the C++ coroutine machinery.
Definition Task.hpp:209
Handle GetHandle() const noexcept
Definition Task.hpp:267
bool IsReady() const noexcept
Definition Task.hpp:261
Task() noexcept=default
Constructs an empty Task that owns no coroutine.
bool IsValid() const noexcept
Definition Task.hpp:255
Task & operator=(Task &&other) noexcept
Definition Task.hpp:234
std::coroutine_handle< promise_type > Handle
The typed coroutine handle owned by this Task.
Definition Task.hpp:212
Task(Task &&other) noexcept
Definition Task.hpp:226