File: task.md

package info (click to toggle)
qcoro 0.12.0-3
  • links: PTS, VCS
  • area: main
  • in suites: forky
  • size: 1,700 kB
  • sloc: cpp: 8,573; python: 32; xml: 26; makefile: 23; sh: 15
file content (203 lines) | stat: -rw-r--r-- 8,701 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
<!--
SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>

SPDX-License-Identifier: GFDL-1.3-or-later
-->

# QCoro::Task

{{ doctable("Coro", "QCoroTask") }}

```cpp
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.

```cpp
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.

```cpp
QCoro::Task<void> getUserDetails(UserID userId) {
    ...

    const QString name = co_await getUserName(userId);

    ...
}
```

!!! info "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

!!! note "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.

---

```cpp
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.

---

```cpp
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:

```cpp
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>`.

```cpp
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;
}
```

!!! info "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

!!! note "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:
```cpp
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.