QCoro::Task
Module | Coro |
---|---|
Include |
|
CMake |
|
QMake |
|
template<typename T> class QCoro::Task
Any coroutine that wants to co_await
one of the types supported by the QCoro library must have
return type QCoro::Task<T>
, where T
is the type of the "regular" coroutine return value.
There's no need by the user to interact with or construct QCoro::Task
manually, the object is
constructed automatically by the compiler before the user code is executed. To return a value
from a coroutine, use co_return
, which will store the result in the Task
object and leave
the coroutine.
QCoro::Task<QString> getUserName(UserID userId) {
...
// Obtain a QString by co_awaiting another coroutine
const QString result = co_await fetchUserNameFromDb(userId);
...
// Return the QString from the coroutine as you would from a regular function,
// just use `co_return` instead of `return` keyword.
co_return result;
}
To obtain the result of a coroutine that returns QCoro::Task<T>
, the result must be co_await
ed.
When the coroutine co_return
s a result, the result is stored in the Task
object and the co_await
ing
coroutine is resumed. The result is obtained from the returned Task
object and returned as a result
of the co_await
call.
QCoro::Task<void> getUserDetails(UserID userId) {
...
const QString name = co_await getUserName(userId);
...
}
Exception Propagation
When coroutines throws an unhandled exception, the exception is stored in the Task
object and
is re-thrown from the co_await
call in the awaiting coroutine.
Note that a default-constructed Task<T>
object is not associated with any coroutine and awaiting
on it will suspend the awaiter indefinitely. Moving into a default constructed Task<T>
associates
it with the coroutine previously associated with the moved-from Task<T>
, and so awaiting on the
moved-to Task<T>
will work as expcted. On the other hand, the moved-from Task<T>
will no longer
be associated with any coroutine and awaiting on it will behave the same as awaiting a default-
constructed task - it will suspend the awaiter indefinitely.
then()
continuation
This feature is available since QCoro 0.5.0
Sometimes it's not possible to co_await
a coroutine, for example when calling a coroutine from a
reimplementation of a virtual function from a 3rd party library, where we cannot change the signature
of that function to be a coroutine (e.g. a reimplementation of QAbstractItemModel::data()
).
Even in this case, we want to process the result of the coroutine asynchronously, though. For such
cases, Task<T>
provides a then()
member function that allows the caller to provide a custom
continuation callback to be invoked when the coroutine finishes.
template<typename ThenCallback>
requires (std::invocable<ThenCallback> || (!std::is_void<T> && std::invocable<ThenCallback, T>))
Task<R> Task<T>::then(ThenCallback callback);
The Task<T>::then()
member function has two arguments. The first argument is the continuation
that is called when the coroutine finishes. The second argument is optional - it is a callable
that gets invoked instead of the continuation when the coroutine throws an exception.
The continuation callback must be a callable that accepts either zero arguments (effectively
discardin the result of the coroutine), or exactly one argument of type T
or type implicitly
constructible from T
.
If the return type of the ThenCallback
is void
, then the return type of the then()
functon is
Task<void>
. If the return type of the ThenCallback
is R
or Task<R>
, the return type of the
then()
function is Task<R>
. This means that the ThenCallback
can be a coroutine as well. Thanks
to the return type always being of type Task<R>
, it is possible to chain multiple .then()
calls,
or co_await
the result of the entire chain.
If the coroutine throws an exception, the exception is re-thrown when the result of the entire
continuation is co_await
ed. If the result of the continuation is not co_await
ed, the exception
is silently ignored.
If an exception is thrown from the ThenCallback
, then the exception is either propagated to the nex
chained then()
continuation or re-thrown if directly co_await
ed. If the result is not co_await
ed
and no futher then()
continuation is chained after the one that has thrown, then the exception is
silently ignored.
template<typename ThenCallback, typename ErrorCallback>
requires (((std::is_void_t<T> && std::invocable<ThenCallback>) || std::invocable<ThenCallback, T>)
&& std::invocable<ErrorCallback, const std::exception &>)
Task<R> Task<T>::then(ThenCallback thenCallback, ErrorCallback errorCallback);
An overload of the then()
member function which takes an additional callback to be invoked when
an exception is thrown from the coroutine. The ErrorCallback
must be a callable that takes exactly
one argument, which is const std::exception &
, holding reference to the exception thrown. An exception
thrown from the ErrorCallback
will be re-thrown if the entire continuation is co_await
ed. If another
.then()
continuation is chained after the current continuation and has an ErrorCallback
, then the
ErrorCallback
will be invoked. Otherwise, the exception is silently ignored.
If an exception is thrown by the non-void coroutine and is handled by the ErrorCallback
, then if the
resulting continuation is co_await
ed, the result will be a default-constructed instance of type R
(since the ThenCallback
was unable to provide a proper instance of type R
). If R
is not default-
constructible, the program will not compile. Thus, if returning a non-default-constructible type from
a coroutine that may throw an exception, we recommend to wrap the type in std::optional
.
Examples:
QString User::name() {
if (mName.isNull()) {
mApi.fetchUserName().then(
[this](const QString &name) {
mName = name;
Q_EMIT nameChanged();
}, [](const std::exception &e) {
mName = QStringLiteral("Failed to fetch name: %1").arg(e.what());
Q_EMIT nameChanged();
});
return QStringLiteral("Loading...");
} else {
return mName;
}
}
Blocking wait
Sometimes it's necessary to wait for a coroutine in a blocking manner - this is especially useful
in tests where possibly no event loop is running. QCoro has QCoro::waitFor()
function
which takes QCoro::Task<T>
(that is, result of calling any QCoro-based coroutine) and blocks
until the coroutine finishes. If the coroutine has a non-void return value, the value is returned
from waitFor().
Since QCoro 0.8.0 it is possible to use QCoro::waitFor()
with any awaitable type, not just QCoro::Task<T>
.
QCoro::Task<int> computeAnswer() {
co_await QCoro::sleepFor(std::chrono::years{7'500'00});
co_return 42;
}
void nonCoroutineFunction() {
// The following line will block as if computeAnswer were not a coroutine. It will internally run a
// a QEventLoop, so other events are still processed.
const int answer = QCoro::waitFor(computeAnswer());
std::cout << "The answer is: " << answer << std::endl;
}
Event loops
The implementation internally uses a QEventLoop
to wait for the coroutine to be completed.
This means that a QCoreApplication
instance must exist, although it does not need to be
executed. Usual warnings about using a nested event loop apply here as well.
Interfacing with synchronous functions
This feature is available since QCoro 0.7.0
Sometimes you need to interface with code that is not coroutine-aware, for example when building list models.
If you'd use the normal .then()
function, and the coroutine would finish after the model is deleted, the program would crash.
The QCoro::connect
function is similar to QObject::connect
,
but for QCoro::Task
s.
Just like QObject::connect
, it only calls its callback if the context object still exists.
This is an example for a function that fetches data and updates a model:
void updateModel() {
auto task = someCoroutine();
QCoro::connect(std::move(task), this, [this](auto &&result) {
beginResetModel();
m_entries = std::move(result);
endResetModel();
});
}
If the model is deleted before the coroutine finishes, the connected lambda will not be called.