15 KiB
QtPromise
Promises/A+ implementation for Qt/C++.
Requires Qt 5.4 (or later) with C++11 support enabled.
Getting Started
Installation
QtPromise is a header-only library, simply download the latest release (or git submodule
) and include qtpromise.pri
from your project .pro
.
qpm
Alternatively and only if your project relies on qpm, you can install QtPromise as follow:
qpm install com.github.simonbrunel.qtpromise
Usage
The recommended way to use QtPromise is to include the single module header:
#include <QtPromise>
Example
Let's first make the code more readable by using the library namespace:
using namespace QtPromise;
This download
function creates a promise from callbacks which will be resolved when the network request is finished:
QPromise<QByteArray> download(const QUrl& url)
{
return QPromise<QByteArray>([&](
const QPromiseResolve<QByteArray>& resolve,
const QPromiseReject<QByteArray>& reject) {
QNetworkReply* reply = manager->get(QNetworkRequest(url));
QObject::connect(reply, &QNetworkReply::finished, [=]() {
if (reply->error() == QNetworkReply::NoError) {
resolve(reply->readAll());
} else {
reject(reply->error());
}
reply->deleteLater();
});
});
}
The following method uncompress
data in a separate thread and returns a promise from QFuture:
QPromise<Entries> uncompress(const QByteArray& data)
{
return qPromise(QtConcurrent::run([](const QByteArray& data) {
Entries entries;
// {...} uncompress data and parse content.
if (error) {
throw MalformedException();
}
return entries;
}, data));
}
It's then easy to chain the whole asynchronous process using promises:
- initiate the promise chain by downloading a specific URL,
then
and only if download succeeded, uncompress received data,then
validate and process the uncompressed entries,finally
perform operations whatever the process succeeded or failed,- and handle specific errors using
fail
.
download(url).then(&uncompress).then([](const Entries& entries) {
if (entries.isEmpty()) {
throw UpdateException("No entries");
}
// {...} process entries
}).finally([]() {
// {...} cleanup
}).fail([](QNetworkReply::NetworkError err) {
// {...} handle network error
}).fail([](const UpdateException& err) {
// {...} handle update error
}).fail([]() {
// {...} catch all
});
QtConcurrent
QtPromise integrates with QtConcurrent to make easy chaining QFuture with QPromise.
Convert
Converting QFuture<T>
to QPromise<T>
is done using the qPromise
helper:
QFuture<int> future = QtConcurrent::run([]() {
// {...}
return 42;
});
QPromise<int> promise = qPromise(future);
or simply:
auto promise = qPromise(QtConcurrent::run([]() {
// {...}
}));
Chain
Returning a QFuture<T>
in then
or fail
automatically translate to QPromise<T>
:
QPromise<int> input = ...
auto output = input.then([](int res) {
return QtConcurrent::run([]() {
// {...}
return QString("42");
});
});
// output type: QPromise<QString>
output.then([](const QString& res) {
// {...}
});
The output
promise is resolved when the QFuture
is finished.
Error
Exceptions thrown from a QtConcurrent thread reject the associated promise with the exception as the reason. Note that if you throw an exception that is not a subclass of QException
, the promise with be rejected with QUnhandledException
(this restriction only applies to exceptions thrown from a QtConcurrent thread, read more).
QPromise<int> promise = ...
promise.then([](int res) {
return QtConcurrent::run([]() {
// {...}
if (!success) {
throw CustomException();
}
return QString("42");
});
}).fail(const CustomException& err) {
// {...}
});
Thread-Safety
QPromise is thread-safe and can be copied and accessed across different threads. QPromise relies on explicitly data sharing and thus auto p2 = p1
represents the same promise: when p1
resolves, handlers registered on p1
and p2
are called, the fulfilled value being shared between both instances.
Note: while it's safe to access the resolved value from different threads using
then
, QPromise provides no guarantee about the object being pointed to. Thread-safety and reentrancy rules for that object still apply.
QPromise
QPromise<T>::QPromise(resolver)
Creates a new promise that will be fulfilled or rejected by the given resolver
lambda:
QPromise<int> promise([](const QPromiseResolve<int>& resolve, const QPromiseReject<int>& reject) {
async_method([=](bool success, int result) {
if (success) {
resolve(result);
} else {
reject(customException());
}
});
});
Note:
QPromise<void>
is specialized to not contain any value, meaning that theresolve
callback takes no argument.
C++14
QPromise<int> promise([](const auto& resolve, const auto& reject) {
// {...}
});
QPromise<T>::then(onFulfilled, onRejected) -> QPromise<R>
See Promises/A+ .then
for details.
QPromise<int> input = ...
auto output = input.then([](int res) {
// called with the 'input' result if fulfilled
}, [](const ReasonType& reason) {
// called with the 'input' reason if rejected
// see QPromise<T>::fail for details
});
Note
:
onRejected
handler is optional,output
will be rejected with the same reason asinput
.
Note
: it's recommended to use the
fail
shorthand to handle errors.
The type <R>
of the output
promise depends on the return type of the onFulfilled
handler:
QPromise<int> input = {...}
auto output = input.then([](int res) {
return QString::number(res); // -> QPromise<QString>
});
// output type: QPromise<QString>
output.then([](const QString& res) {
// {...}
});
Note
: only
onFulfilled
can change the promise type,onRejected
must return the same type asonFulfilled
. That also means ifonFulfilled
isnullptr
,onRejected
must return the same type as theinput
promise.
QPromise<int> input = ...
auto output = input.then([](int res) {
return res + 4;
}, [](const ReasonType& reason) {
return -1;
});
If onFulfilled
doesn't return any value, the output
type is QPromise<void>
:
QPromise<int> input = ...
auto output = input.then([](int res) {
// {...}
});
// output type: QPromise<void>
output.then([]() {
// `QPromise<void>` `onFulfilled` handler has no argument
});
You can also decide to skip the promise result by omitting the handler argument:
QPromise<int> input = {...}
auto output = input.then([]( /* skip int result */ ) {
// {...}
});
The output
promise can be rejected by throwing an exception in either onFulfilled
or onRejected
:
QPromise<int> input = {...}
auto output = input.then([](int res) {
if (res == -1) {
throw ReasonType();
} else {
return res;
}
});
// output.isRejected() is true
If an handler returns a promise (or QFuture), the output
promise is delayed and will be resolved by the returned promise.
QPromise<T>::fail(onRejected) -> QPromise<T>
Shorthand to promise.then(nullptr, onRejected)
, similar to the catch
statement:
promise.fail([](const MyException&) {
// {...}
}).fail(const QException&) {
// {...}
}).fail(const std::exception&) {
// {...}
}).fail() {
// {...} catch-all
});
QPromise<T>::finally(handler) -> QPromise<T>
This handler
is always called, without any argument and whatever the input
promise state (fulfilled or rejected). The output
promise has the same type as the input
one but also the same value or error. The finally handler
can not modify the fulfilled value (the returned value is ignored), however, if handler
throws, output
is rejected with the new exception.
auto output = input.finally([]() {
// {...}
});
If handler
returns a promise (or QFuture), the output
promise is delayed until the returned promise is resolved and under the same conditions: the delayed value is ignored, the error transmitted to the output
promise.
QPromise<T>::tap(handler) -> QPromise<T>
This handler
allows to observe the value of the input
promise, without changing the propagated value. The output
promise will be resolved with the same value as the input
promise (the handler
returned value will be ignored). However, if handler
throws, output
is rejected with the new exception. Unlike finally
, this handler is not called for rejections.
QPromise<int> input = {...}
auto output = input.tap([](int res) {
log(res);
}).then([](int res) {
// {...}
});
If handler
returns a promise (or QFuture), the output
promise is delayed until the returned promise is resolved and under the same conditions: the delayed value is ignored, the error transmitted to the output
promise.
QPromise<T>::delay(handler) -> QPromise<T>
This method returns a promise that will be fulfilled with the same value as the input
promise and after at least msec
milliseconds. If the input
promise is rejected, the output
promise is immediately rejected with the same reason.
QPromise<int> input = {...}
auto output = input.delay(2000).then([](int res) {
// called 2 seconds after `input` is fulfilled
});
QPromise<T>::wait() -> QPromise<T>
This method holds the execution of the remaining code without blocking the event loop of the current thread:
int result = -1;
QPromise<int> input = qPromise(QtConcurrent::run([]() { return 42; }));
auto output = input.then([&](int res) {
result = res;
});
// output.isPending() is true && result is -1
output.wait();
// output.isPending() is false && result is 42
QPromise<T>::isPending() -> bool
Returns true
if the promise is pending (not fulfilled or rejected), otherwise returns false
.
QPromise<T>::isFulfilled() -> bool
Returns true
if the promise is fulfilled, otherwise returns false
.
QPromise<T>::isRejected() -> bool
Returns true
if the promise is rejected, otherwise returns false
.
QPromise (statics)
[static] QPromise<T>::resolve(value) -> QPromise<T>
Creates a QPromise<T>
that is fulfilled with the given value
of type T
:
QPromise<int> compute(const QString& type)
{
if (type == "magic") {
return QPromise<int>::resolve(42);
}
return QPromise<int>([](const QPromiseResolve<int>& resolve) {
// {...}
});
}
See also: qPromise
[static] QPromise<T>::reject(reason) -> QPromise<T>
Creates a QPromise<T>
that is rejected with the given reason
of whatever type:
QPromise<int> compute(const QString& type)
{
if (type == "foobar") {
return QPromise<int>::reject(QString("Unknown type: %1").arg(type));
}
return QPromise<int>([](const QPromiseResolve<int>& resolve) {
// {...}
});
}
[static] QPromise<T>::all(QVector<QPromise<T>>) -> QPromise<QVector<T>>
Returns a QPromise<QVector<T>>
that fulfills when all promises
of (the same) type T
have been fulfilled. The output
value is a vector containing all the values of promises
, in the same order. If any of the given promises
fail, output
immediately rejects with the error of the promise that rejected, whether or not the other promises are resolved.
QVector<QPromise<QByteArray> > promises{
download(QUrl("http://a...")),
download(QUrl("http://b...")),
download(QUrl("http://c..."))
};
auto output = QPromise<QByteArray>::all(promises);
// output type: QPromise<QVector<QByteArray>>
output.then([](const QVector<QByteArray>& res) {
// {...}
});
See also: qPromiseAll
Helpers
qPromise(T value) -> QPromise<R>
Similar to the QPromise<T>::resolve
static method, creates a promise resolved from a given value
without the extra typing:
auto promise = qPromise(); // QPromise<void>
auto promise = qPromise(42); // QPromise<int>
auto promise = qPromise(QString("foo")); // QPromise<QString>
This method also allows to convert QFuture<T>
to QPromise<T>
delayed until the QFuture
is finished (read more).
qPromiseAll(QVector<QPromise<T> promises) -> QPromise<QVector<T>>
This method simply calls the appropriated QPromise<T>::all
static method based on the given QVector
type. In some cases, this method is more convenient than the static one since it avoid some extra typing:
QVector<QPromise<QByteArray> > promises{...}
auto output = qPromiseAll(promises);
// eq. QPromise<QByteArray>::all(promises)
License
QtPromise is available under the MIT license.