libQuotient
A Qt library for building matrix clients
jobhandle.h
Go to the documentation of this file.
1 #pragma once
2 
3 #include "basejob.h"
4 
5 #include <QtCore/QFuture>
6 
7 namespace Quotient {
8 
9 template <typename FnT, typename JobT>
10 concept BoundResultHandler = std::invocable<FnT, JobT*> || std::invocable<FnT>
11  || requires(FnT f, JobT j) { f(collectResponse(&j)); };
12 
13 template <typename FnT, typename JobT>
14 concept ResultHandler = BoundResultHandler<FnT, JobT> || std::is_member_function_pointer_v<FnT>;
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.
72 template <class JobT>
73 class QUOTIENT_API JobHandle : public QPointer<JobT>, public QFuture<JobT*> {
74 public:
75  using pointer_type = QPointer<JobT>;
76  using future_value_type = JobT*;
77  using future_type = QFuture<future_value_type>;
78 
79 private:
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 
83  JobHandle(JobT* job, future_type&& futureToWrap)
84  : pointer_type(job), future_type(std::move(futureToWrap))
85  {}
86 
87  static future_type setupFuture(JobT* job)
88  {
89  return job ? job->future().then([job] { return future_value_type{ job }; }) : future_type{};
90  }
91 
92 public:
93  Q_IMPLICIT JobHandle(JobT* job = nullptr) : JobHandle(job, setupFuture(job)) {}
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>
122  auto onResult(ConfigT config, FnT&& fn)
123  {
124  return rewrap(future_type::then(config, continuation(std::forward<FnT>(fn), config)));
125  }
126 
127  //! The overload for onResult matching 1-arg QFuture::then
128  template <BoundResultHandler<JobT> FnT>
129  auto onResult(FnT&& fn)
130  {
131  return rewrap(future_type::then(continuation(std::forward<FnT>(fn))));
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().
141  template <typename ConfigT, ResultHandler<JobT> SuccessFnT, ResultHandler<JobT> FailureFnT = Skip>
142  auto then(ConfigT config, SuccessFnT&& onSuccess, FailureFnT&& onFailure = {})
143  requires requires(future_type f) { f.then(config, Skip{}); }
144  {
145  return rewrap(future_type::then(
146  config, combineContinuations(std::forward<SuccessFnT>(onSuccess),
147  std::forward<FailureFnT>(onFailure), config)));
148  }
149 
150  //! The overload making the combined continuation as if with 1-arg QFuture::then
151  template <BoundResultHandler<JobT> SuccessFnT, BoundResultHandler<JobT> FailureFnT = Skip>
152  auto then(SuccessFnT&& onSuccess, FailureFnT&& onFailure = {})
153  {
154  return rewrap(future_type::then(combineContinuations(std::forward<SuccessFnT>(onSuccess),
155  std::forward<FailureFnT>(onFailure))));
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>
167  auto onFailure(FnT&& fn)
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>
175  auto onCanceled(QObject* context, FnT&& fn)
176  {
177  return rewrap(
178  future_type::onCanceled(context, bindToContext(std::forward<FnT>(fn), context)));
179  }
180 
181  //! Same as QFuture::onCanceled but accepts QObject-derived member functions and rewraps
182  //! returned values
183  template <typename FnT>
184  auto onCanceled(FnT&& fn)
185  {
186  return rewrap(future_type::onCanceled(BoundFn{ std::forward<FnT>(fn) }));
187  }
188 
189  //! Get a QFuture for the value returned by `collectResponse()` called on the underlying job
190  auto responseFuture()
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)) {
204  Q_ASSERT(QThread::currentThread() == pJob->thread());
205  pJob->abandon(); // Triggers cancellation of the future
206  }
207  }
208 
209 private:
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>
217  auto callFn(future_value_type job)
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(
227  requires { fn(collectResponse(job)); },
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.
239  [[no_unique_address]]
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.
253  if constexpr (std::derived_from<std::remove_pointer_t<ConfigT>, QObject>
254  && std::is_member_function_pointer_v<FnT>) {
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 
272  template <ResultHandler<JobT> SuccessFnT, ResultHandler<JobT> FailureFnT, typename ConfigT = Skip>
273  static auto combineContinuations(SuccessFnT&& onSuccess, FailureFnT&& onFailure,
274  ConfigT config = {})
275  {
276  return [sFn = bindToContext(std::forward<SuccessFnT>(onSuccess), config),
277  fFn = bindToContext(std::forward<FailureFnT>(onFailure), config)](
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>
297  auto rewrap(QFuture<JobHandle<NewJobT>> ft)
298  -> QFuture<typename JobHandle<NewJobT>::future_value_type>
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.
314  QFutureInterface<typename JobHandle<NewJobT>::future_value_type> newPromise(
315  QFutureInterfaceBase::State::Pending);
316  ft.then([newPromise](JobHandle<NewJobT> nestedHandle) mutable {
317  Q_ASSERT(nestedHandle.isStarted());
318  newPromise.reportStarted();
319  nestedHandle.then([newPromise]() mutable { newPromise.reportFinished(); })
320  .onCanceled([newPromise]() mutable { newPromise.cancelAndFinish(); });
321  }).onCanceled([newPromise]() mutable {
322  newPromise.reportStarted();
323  newPromise.cancelAndFinish();
324  });
325  return newPromise.future();
326  }
327 
328  static auto rewrap(auto someOtherFuture) { return someOtherFuture; }
329 };
330 
331 template <std::derived_from<BaseJob> JobT>
332 JobHandle(JobT*) -> JobHandle<JobT>;
333 
334 } // namespace Quotient
335 
336 Q_DECLARE_SMART_POINTER_METATYPE(Quotient::JobHandle)