libQuotient
A Qt library for building matrix clients
Loading...
Searching...
No Matches
jobhandle.h
Go to the documentation of this file.
1#pragma once
2
3#include "basejob.h"
4
5#include <QtCore/QFuture>
6#include <QtCore/QPointer>
7
8namespace Quotient {
9
10template <typename FnT, typename JobT>
11concept BoundResultHandler = std::invocable<FnT, JobT*> || std::invocable<FnT>
12 || requires(FnT f, JobT j) { f(collectResponse(&j)); };
13
14template <typename FnT, typename JobT>
16
17//! \brief A job pointer and a QFuture in a single package
18//!
19//! This class wraps a pointer to any job the same way QPointer does: it turns to nullptr when
20//! the job is destroyed. On top of that though, it provides you with an interface of QFuture as-if
21//! obtained by calling `QtFuture::connect(job, &BaseJob::result).then([job] { return job; });`
22//! before any other slot is connected to it. In the end, you (still) get the interface of \p JobT
23//! at `handle->`, and `handle.` gives you the interface (very close to that, read below for
24//! differences) of `QFuture<JobT*>`.
25//!
26//! You can mix usage of the two interfaces, bearing in mind that any continuation attached via
27//! the future interface will overtake anything connected to `BaseJob::result` but come behind
28//! anything connected to `BaseJob::finished` (that applies to `onCanceled()`, too).
29//!
30//! QFuture is somewhat rigid in terms of what it accepts for (normal, i.e. not cancelled)
31//! continuations: the continuation function must accept a single argument of the type carried by
32//! the future, or of the future type itself. JobHandle allows normal continuation functions (i.e.
33//! those passed to `then()`, `onResult()` and `onFailure()`) to accept:
34//! - no parameters;
35//! - `JobT*` or any pointer it is convertible to (`const JobT*`, `BaseJob*` etc.);
36//! - the value returned by calling `collectResponse()` with the above pointer as the parameter.
37//!
38//! Aside from free functions and function objects (including lambdas), you can also pass member
39//! functions of QObject-derived classes (`connect()` slot style) to all continuations, including
40//! onCanceled().
41//!
42//! JobHandle doesn't support passing its full type to continuation functions like QFuture does,
43//! as there's no case for that (we don't need to deal with exceptions).
44//!
45//! This extended interface helps with migration of the current code that `connect()`s to the job
46//! completion signals. The existing code will (mostly) run fine without changes; the only thing
47//! that will stop working is using `auto*` for a variable initialised from `Connection::callApi`
48//! (plain `auto` still works). If you want to migrate the existing code to the future-like
49//! interface, just replace:
50//! \code
51//! auto j = callApi<Job>(jobParams...);
52//! connect(j, &BaseJob::result, object, slot);
53//! \endcode
54//! with `callApi<Job>(jobParams...).onResult(object, slot);` - that's all. If you have a connection
55//! to `BaseJob::success`, use `then()` instead of `onResult()`, and if you only connect to
56//! `BaseJob::failure`, `onFailure()` is at your service. And you can also combine the two using
57//! `then()`, e.g.:
58//! \code
59//! callApi<Job>(jobParams...).then([this] { /* on success... */ },
60//! [this] { /* on failure... */ });
61//! \endcode
62//!
63//! One more extension to QFuture is the way the returned value is treated:
64//! - if your function returns `void` the continuation will have type `JobHandler<JobT>` and carry
65//! the same pointer as before;
66//! - if your function returns a `JobHandle` (e.g. from another call to `Connection::callApi`),
67//! it will be automatically rewrapped into a `QFuture`, because `QFuture<JobHandle<AnotherJobT>>`
68//! is rather unwieldy for any intents and purposes, and `JobHandle<AnotherJobT>` would have
69//! a very weird QPointer interface as that new job doesn't even exist when continuation is
70//! constructed;
71//! - otherwise, the return value is wrapped in a "normal" QFuture, JobHandle waves you good-bye and
72//! further continuations will follow pristine QFuture rules.
73template <class JobT>
74class QUOTIENT_API JobHandle : public QPointer<JobT>, public QFuture<JobT*> {
75public:
79
80private:
81 //! A placeholder structure with a private type, co-sitting as a no-op function object
82 struct Skip : public decltype([](future_value_type) {}) {};
83
86 {}
87
88public:
89 JobHandle() = default; //!< Creates a "null QPointer, cancelled QFuture" job handle
90
91 //! Create a new job and get a JobHandle for it
92 template <typename... JobArgTs>
94 {
95 auto pJob = new JobT(std::forward<JobArgTs>(jobArgs)...);
96 return { pJob, pJob->future().then([pJob] { return future_value_type{ pJob }; }) };
97 }
98
99 //! \brief Attach a continuation to a successful or unsuccessful completion of the future
100 //!
101 //! The continuation passed via \p fn should be an invokable that accepts one of the following:
102 //! 1) no arguments - this is meant to simplify transition from job completion handlers
103 //! connect()ed to BaseJob::result, BaseJob::success or BaseJob::failure.
104 //! 2) a pointer to a (const, if you want) job object - this can be either `BaseJob*`,
105 //! `JobT*` (recommended), or anything in between. Unlike slot functions connected
106 //! to BaseJob signals, this option allows you to access the specific job type so you don't
107 //! need to carry the original job pointer in a lambda - JobHandle does it for you. This is
108 //! meant to be a transitional form on the way to (3); eventually we should migrate to
109 //! (1)+(3) entirely.
110 //! 3) the type returned by `collectResponse()` if it's well-formed (it is for all generated
111 //! jobs, needs overloading for manual jobs).
112 //!
113 //! \note The continuation returned from onResult() will not be triggered if/when the future is
114 //! cancelled or the underlying job is abandoned; use onCanceled() to catch cancellations.
115 //! You can also connect to BaseJob::finished using the QPointer interface of the handle
116 //! if you need to do something before _any_ continuation attached to his job kicks in.
117 //!
118 //! \param config passed directly to QFuture::then() as the first argument (see
119 //! the documentation on QFuture::then() for accepted types) and can also be used
120 //! as the object for a slot-like member function in QObject::connect() fashion
121 //! \param fn the continuation function to attach to the future; can be a member function
122 //! if \p config is a pointer to an QObject-derived class
123 //! \return if \p fn returns `void`, a new JobHandle for the same job, with the continuation
124 //! attached; otherwise, the return value of \p fn wrapped in a plain QFuture
125 template <typename ConfigT, ResultHandler<JobT> FnT>
127 {
129 }
130
131 //! The overload for onResult matching 1-arg QFuture::then
132 template <BoundResultHandler<JobT> FnT>
134 {
136 }
137
138 //! \brief Attach continuations depending on the job success or failure
139 //!
140 //! This is inspired by `then()` in JavaScript; beyond the first argument passed through to
141 //! `QFuture::then`, it accepts two more arguments (\p onFailure is optional), combining them
142 //! in a single continuation: if the job ends with success, \p onSuccess is called; if the job
143 //! fails, \p onFailure is called. The requirements to both functions are the same as to the
144 //! single function passed to onResult().
148 {
149 return rewrap(future_type::then(
152 }
153
154 //! The overload making the combined continuation as if with 1-arg QFuture::then
157 {
160 }
161
162 //! Same as then(config, [] {}, fn)
163 template <typename FnT>
164 auto onFailure(auto config, FnT&& fn)
165 {
166 return then(config, Skip{}, std::forward<FnT>(fn));
167 }
168
169 //! Same as then([] {}, fn)
170 template <typename FnT>
172 {
173 return then(Skip{}, std::forward<FnT>(fn));
174 }
175
176 //! Same as QFuture::onCanceled but accepts QObject-derived member functions and rewraps
177 //! returned values
178 template <typename FnT>
180 {
181 return rewrap(
183 }
184
185 //! Same as QFuture::onCanceled but accepts QObject-derived member functions and rewraps
186 //! returned values
187 template <typename FnT>
189 {
191 }
192
193 //! Get a QFuture for the value returned by `collectResponse()` called on the underlying job
195 {
196 return future_type::then([](auto* j) { return collectResponse(j); });
197 }
198
199 //! \brief Abandon the underlying job, if there's one pending
200 //!
201 //! Unlike cancel() that only applies to the current future object but not the upstream chain,
202 //! this actually goes up to the job and calls abandon() on it, thereby cancelling the entire
203 //! chain of futures attached to it.
204 //! \sa BaseJob::abandon
205 void abandon()
206 {
207 if (auto pJob = pointer_type::get(); isJobPending(pJob)) {
209 pJob->abandon(); // Triggers cancellation of the future
210 }
211 }
212
213private:
214 //! A function object that can be passed to QFuture::then and QFuture::onCanceled
215 template <typename FnT>
216 struct BoundFn {
217 auto operator()() { return callFn<false>(nullptr); } // For QFuture::onCanceled
218 auto operator()(future_value_type job) { return callFn(job); } // For QFuture::then
219
220 template <bool AllowJobArg = true>
222 {
223 if constexpr (std::invocable<FnT>) {
224 return std::forward<FnT>(fn)();
225 } else {
226 static_assert(AllowJobArg, "onCanceled continuations should not accept arguments");
227 if constexpr (requires { fn(job); })
228 return fn(job);
229 else if constexpr (requires { collectResponse(job); }) {
230 static_assert(
232 "The continuation function must accept either of: 1) no arguments; "
233 "2) the job pointer itself; 3) the value returned by collectResponse(job)");
234 return fn(collectResponse(job));
235 }
236 }
237 }
238
239 // See https://www.cppstories.com/2021/no-unique-address/
240#ifndef Q_CC_CLANG
241 // Apple Clang crashes with ICE and vanilla Clang 17 generates faulty code if fn has no
242 // unique address. https://github.com/llvm/llvm-project/issues/59831 might be related.
244#endif
245 FnT fn;
246 };
247
248 template <typename FnT>
249 BoundFn(FnT&&) -> BoundFn<FnT>;
250
251 template <typename FnT, typename ConfigT = Skip>
252 static auto bindToContext(FnT&& fn, ConfigT config = {})
253 {
254 // Even though QFuture::then() and QFuture::onCanceled() can use context QObjects
255 // to determine the execution thread, they cannot bind slots to these context objects,
256 // the way QObject::connect() does; so we do it here.
259 return BoundFn{ std::bind_front(std::forward<FnT>(fn), config) };
260 } else
261 return BoundFn{ std::forward<FnT>(fn) };
262 }
263
264 template <ResultHandler<JobT> FnT, typename ConfigT = Skip>
265 static auto continuation(FnT&& fn, ConfigT config = {})
266 {
267 return [f = bindToContext(std::forward<FnT>(fn), config)](future_value_type arg) mutable {
268 if constexpr (std::is_void_v<decltype(f(arg))>) {
269 f(arg);
270 return arg;
271 } else
272 return f(arg);
273 };
274 }
275
278 ConfigT config = {})
279 {
282 future_value_type job) mutable {
283 using sType = decltype(sFn(job));
284 using fType = decltype(fFn(job));
285 if constexpr (std::is_void_v<sType> && std::is_void_v<fType>)
286 return (job->status().good() ? sFn(job) : fFn(job), job);
287 else if constexpr (std::is_same_v<FailureFnT, Skip>) {
288 // Still call fFn to suppress unused lambda warning
289 return job->status().good() ? sFn(job) : (fFn(job), sType{});
290 } else
291 return job->status().good() ? sFn(job) : fFn(job);
292 };
293 }
294
295 auto rewrap(future_type&& ft) const
296 {
297 return JobHandle(pointer_type::get(), std::move(ft));
298 }
299
300 template <typename NewJobT>
303 {
304 // When a continuation function returns a job handle (e.g. by invoking callApi() inside of
305 // it) that handle ends up being wrapped in a QFuture by QFuture::then() or
306 // QFuture::onCanceled() called internally from their JobHandle counterparts. In a pure
307 // QFuture world, there's QFuture::unwrap() to flatten the nested futures; unfortunately,
308 // QFuture::unwrap() requires the nested object to be exactly a QFuture, not anything
309 // derived from it. This method basically does what QFuture::unwrap() does, but is much
310 // simpler because we don't need to deal with exceptions and already know the nested type.
311 // It still returns QFuture and not JobHandle. Were it a JobHandle, its QPointer interface
312 // would be rather confusing: initially it would be nullptr because the job doesn't even
313 // exist when the continuation is constructed, and only later it would change its value
314 // to something useful. Unless the client code stored the original JobHandle, it would
315 // lose that change and only store nullptr; and if it stores a JobHandle then it can just
316 // use the QFuture interface instead. The returned QFuture settles when the underlying job
317 // finishes or gets cancelled.
324 .onCanceled([newPromise]() mutable { newPromise.cancelAndFinish(); });
325 }).onCanceled([newPromise]() mutable {
328 });
329 return newPromise.future();
330 }
331
332 static auto rewrap(auto someOtherFuture) { return someOtherFuture; }
333};
334
335template <std::derived_from<BaseJob> JobT>
337
338} // namespace Quotient
339
A job pointer and a QFuture in a single package.
Definition jobhandle.h:74
#define QUOTIENT_API