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>
217 struct BoundFn {
218 auto operator()() { return callFn<false>(nullptr); } // For QFuture::onCanceled
219 auto operator()(future_value_type job) { return callFn(job); } // For QFuture::then
220
221 template <bool AllowJobArg = true>
223 {
224 if constexpr (std::invocable<FnT>) {
225 return std::forward<FnT>(fn)();
226 } else {
227 static_assert(AllowJobArg, "onCanceled continuations should not accept arguments");
228 if constexpr (requires { fn(job); })
229 return fn(job);
230 else if constexpr (requires { collectResponse(job); }) {
231 static_assert(
233 "The continuation function must accept either of: 1) no arguments; "
234 "2) the job pointer itself; 3) the value returned by collectResponse(job)");
235 return fn(collectResponse(job));
236 }
237 }
238 }
239
240 // See https://www.cppstories.com/2021/no-unique-address/
241#ifndef Q_CC_CLANG
242 // Apple Clang crashes with ICE and vanilla Clang 17 generates faulty code if fn has no
243 // unique address. https://github.com/llvm/llvm-project/issues/59831 might be related.
245#endif
246 FnT fn;
247 };
248
249 template <typename FnT>
250 BoundFn(FnT&&) -> BoundFn<FnT>;
251
252 template <typename FnT>
253 BoundFn(const FnT &) -> BoundFn<FnT>;
254
255 template <typename FnT, typename ConfigT = Skip>
256 static auto bindToContext(FnT&& fn, ConfigT config = {})
257 {
258 // Even though QFuture::then() and QFuture::onCanceled() can use context QObjects
259 // to determine the execution thread, they cannot bind slots to these context objects,
260 // the way QObject::connect() does; so we do it here.
263 return BoundFn{ std::bind_front(std::forward<FnT>(fn), config) };
264 } else
265 return BoundFn{ std::forward<FnT>(fn) };
266 }
267
268 template <ResultHandler<JobT> FnT, typename ConfigT = Skip>
269 static auto continuation(FnT&& fn, ConfigT config = {})
270 {
271 return [f = bindToContext(std::forward<FnT>(fn), config)](future_value_type arg) mutable {
272 if constexpr (std::is_void_v<decltype(f(arg))>) {
273 f(arg);
274 return arg;
275 } else
276 return f(arg);
277 };
278 }
279
282 ConfigT config = {})
283 {
286 future_value_type job) mutable {
287 using sType = decltype(sFn(job));
288 using fType = decltype(fFn(job));
289 if constexpr (std::is_void_v<sType> && std::is_void_v<fType>)
290 return (job->status().good() ? sFn(job) : fFn(job), job);
291 else if constexpr (std::is_same_v<FailureFnT, Skip>) {
292 // Still call fFn to suppress unused lambda warning
293 return job->status().good() ? sFn(job) : (fFn(job), sType{});
294 } else
295 return job->status().good() ? sFn(job) : fFn(job);
296 };
297 }
298
299 auto rewrap(future_type&& ft) const
300 {
301 return JobHandle(pointer_type::get(), std::move(ft));
302 }
303
304 template <typename NewJobT>
307 {
308 // When a continuation function returns a job handle (e.g. by invoking callApi() inside of
309 // it) that handle ends up being wrapped in a QFuture by QFuture::then() or
310 // QFuture::onCanceled() called internally from their JobHandle counterparts. In a pure
311 // QFuture world, there's QFuture::unwrap() to flatten the nested futures; unfortunately,
312 // QFuture::unwrap() requires the nested object to be exactly a QFuture, not anything
313 // derived from it. This method basically does what QFuture::unwrap() does, but is much
314 // simpler because we don't need to deal with exceptions and already know the nested type.
315 // It still returns QFuture and not JobHandle. Were it a JobHandle, its QPointer interface
316 // would be rather confusing: initially it would be nullptr because the job doesn't even
317 // exist when the continuation is constructed, and only later it would change its value
318 // to something useful. Unless the client code stored the original JobHandle, it would
319 // lose that change and only store nullptr; and if it stores a JobHandle then it can just
320 // use the QFuture interface instead. The returned QFuture settles when the underlying job
321 // finishes or gets cancelled.
328 .onCanceled([newPromise]() mutable { newPromise.cancelAndFinish(); });
329 }).onCanceled([newPromise]() mutable {
332 });
333 return newPromise.future();
334 }
335
336 static auto rewrap(auto someOtherFuture) { return someOtherFuture; }
337};
338
339template <std::derived_from<BaseJob> JobT>
341
342} // namespace Quotient
343
A job pointer and a QFuture in a single package.
Definition jobhandle.h:74
#define QUOTIENT_API