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