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
8
namespace
Quotient
{
9
10
template
<
typename
FnT,
typename
JobT>
11
concept BoundResultHandler =
std
::
invocable
<
FnT
,
JobT
*> ||
std
::
invocable
<
FnT
>
12
||
requires
(
FnT
f
,
JobT
j
) {
f
(
collectResponse
(&
j
)); };
13
14
template
<
typename
FnT,
typename
JobT>
15
concept ResultHandler =
BoundResultHandler
<
FnT
,
JobT
> ||
std
::
is_member_function_pointer_v
<
FnT
>;
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.
73
template
<
class
JobT
>
74
class
QUOTIENT_API
JobHandle
:
public
QPointer
<
JobT
>,
public
QFuture
<
JobT
*> {
75
public
:
76
using
pointer_type
=
QPointer
<
JobT
>;
77
using
future_value_type
=
JobT
*;
78
using
future_type
=
QFuture
<
future_value_type
>;
79
80
private
:
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
84
JobHandle
(
JobT
*
job
,
future_type
&&
futureToWrap
)
85
:
pointer_type
(
job
),
future_type
(
std
::
move
(
futureToWrap
))
86
{}
87
88
public
:
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
>
93
static
JobHandle
createFrom
(
JobArgTs
&&...
jobArgs
)
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
>
126
auto
onResult
(
ConfigT
config
,
FnT
&&
fn
)
127
{
128
return
rewrap
(
future_type
::
then
(
config
,
continuation
(
std
::
forward
<
FnT
>(
fn
),
config
)));
129
}
130
131
//! The overload for onResult matching 1-arg QFuture::then
132
template
<
BoundResultHandler
<
JobT
>
FnT
>
133
auto
onResult
(
FnT
&&
fn
)
134
{
135
return
rewrap
(
future_type
::
then
(
continuation
(
std
::
forward
<
FnT
>(
fn
))));
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().
145
template
<
typename
ConfigT
,
ResultHandler
<
JobT
>
SuccessFnT
,
ResultHandler
<
JobT
>
FailureFnT
=
Skip
>
146
auto
then
(
ConfigT
config
,
SuccessFnT
&&
onSuccess
,
FailureFnT
&&
onFailure
= {})
147
requires
requires
(
future_type
f
) {
f
.
then
(
config
,
Skip
{}); }
148
{
149
return
rewrap
(
future_type
::
then
(
150
config
,
combineContinuations
(
std
::
forward
<
SuccessFnT
>(
onSuccess
),
151
std
::
forward
<
FailureFnT
>(
onFailure
),
config
)));
152
}
153
154
//! The overload making the combined continuation as if with 1-arg QFuture::then
155
template
<
BoundResultHandler
<
JobT
>
SuccessFnT
,
BoundResultHandler
<
JobT
>
FailureFnT
=
Skip
>
156
auto
then
(
SuccessFnT
&&
onSuccess
,
FailureFnT
&&
onFailure
= {})
157
{
158
return
rewrap
(
future_type
::
then
(
combineContinuations
(
std
::
forward
<
SuccessFnT
>(
onSuccess
),
159
std
::
forward
<
FailureFnT
>(
onFailure
))));
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
>
171
auto
onFailure
(
FnT
&&
fn
)
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
>
179
auto
onCanceled
(
QObject
*
context
,
FnT
&&
fn
)
180
{
181
return
rewrap
(
182
future_type
::
onCanceled
(
context
,
bindToContext
(
std
::
forward
<
FnT
>(
fn
),
context
)));
183
}
184
185
//! Same as QFuture::onCanceled but accepts QObject-derived member functions and rewraps
186
//! returned values
187
template
<
typename
FnT
>
188
auto
onCanceled
(
FnT
&&
fn
)
189
{
190
return
rewrap
(
future_type
::
onCanceled
(
BoundFn
{
std
::
forward
<
FnT
>(
fn
) }));
191
}
192
193
//! Get a QFuture for the value returned by `collectResponse()` called on the underlying job
194
auto
responseFuture
()
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
)) {
208
Q_ASSERT
(
QThread
::
currentThread
() ==
pJob
->
thread
());
209
pJob
->
abandon
();
// Triggers cancellation of the future
210
}
211
}
212
213
private
:
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
>
221
auto
callFn
(
future_value_type
job
)
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
(
231
requires
{
fn
(
collectResponse
(
job
)); },
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.
243
[[
no_unique_address
]]
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.
257
if
constexpr
(
std
::
derived_from
<
std
::
remove_pointer_t
<
ConfigT
>,
QObject
>
258
&&
std
::
is_member_function_pointer_v
<
FnT
>) {
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
276
template
<
ResultHandler
<
JobT
>
SuccessFnT
,
ResultHandler
<
JobT
>
FailureFnT
,
typename
ConfigT
=
Skip
>
277
static
auto
combineContinuations
(
SuccessFnT
&&
onSuccess
,
FailureFnT
&&
onFailure
,
278
ConfigT
config
= {})
279
{
280
return
[
sFn
=
bindToContext
(
std
::
forward
<
SuccessFnT
>(
onSuccess
),
config
),
281
fFn
=
bindToContext
(
std
::
forward
<
FailureFnT
>(
onFailure
),
config
)](
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
>
301
auto
rewrap
(
QFuture
<
JobHandle
<
NewJobT
>>
ft
)
302
->
QFuture
<
typename
JobHandle
<
NewJobT
>::
future_value_type
>
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.
318
QFutureInterface
<
typename
JobHandle
<
NewJobT
>::
future_value_type
>
newPromise
(
319
QFutureInterfaceBase
::
State
::
Pending
);
320
ft
.
then
([
newPromise
](
JobHandle
<
NewJobT
>
nestedHandle
)
mutable
{
321
Q_ASSERT
(
nestedHandle
.
isStarted
());
322
newPromise
.
reportStarted
();
323
nestedHandle
.
then
([
newPromise
]()
mutable
{
newPromise
.
reportFinished
(); })
324
.
onCanceled
([
newPromise
]()
mutable
{
newPromise
.
cancelAndFinish
(); });
325
}).
onCanceled
([
newPromise
]()
mutable
{
326
newPromise
.
reportStarted
();
327
newPromise
.
cancelAndFinish
();
328
});
329
return
newPromise
.
future
();
330
}
331
332
static
auto
rewrap
(
auto
someOtherFuture
) {
return
someOtherFuture
; }
333
};
334
335
template
<
std
::
derived_from
<
BaseJob
>
JobT
>
336
JobHandle
(
JobT
*) ->
JobHandle
<
JobT
>;
337
338
}
// namespace Quotient
339
340
Q_DECLARE_SMART_POINTER_METATYPE
(
Quotient
::
JobHandle
)
Quotient::JobHandle
A job pointer and a QFuture in a single package.
Definition
jobhandle.h:74
Quotient
Definition
accountregistry.h:13
QUOTIENT_API
#define QUOTIENT_API
Definition
quotient_export.h:22
Quotient
jobs
jobhandle.h
Generated by
1.9.8