Skip to content

Commit 42f3427

Browse files
author
Damian Nowakowski
committed
patches
1 parent 440a2b1 commit 42f3427

File tree

1 file changed

+128
-46
lines changed

1 file changed

+128
-46
lines changed

README.md

+128-46
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ If you notice any issue or have any idea how to improve the following examples f
99
# Index
1010
* [What is a coroutine](#what-is-a-coroutine)
1111
* [The simpliest c++ coroutine example possible](#the-simpliest-c-coroutine-example-possible)
12-
* [Coroutine Tasks](#coroutine-tasks)
12+
* [Awaiters](#awaiters)
1313
* [Generators - coroutines returning values](#generators---coroutines-returning-values)
14+
* [Fibonnaci Generator](#fibonacci-generator)
15+
* [co_return value](#co_return-value)
1416
* [Camera Fade Out for Unreal Engine 5](#camera-fade-out-for-unreal-engine-5)
17+
* [More coroutines for UE5](#more-coroutines-for-ue5)
18+
* [Stability concerns](#stability-concerns)
1519

1620
# What is a coroutine?
1721

@@ -103,7 +107,7 @@ Now let's go through all of this from top to bottom. First of all we have to def
103107
## Coroutine Handle
104108
This is a structure based on the `std::coroutine_handle`. It controls the flow of the coroutine and will be used to resume the suspended coroutine later. The absolute minimum which needs to be defined inside the Handle are:
105109
* `promise_type` - this is a type of the coroutine Promise. What is a Promise will be explained later.
106-
* `await_resume` - this function is called when the coroutine is resumed.
110+
* `await_resume` - this function is called when the coroutine is resumed. It can return `void` or a value of any type. When returning a value, the value will be obtained when calling `co_await` (e.g. `auto Value = co_await CoroHandle();`)
107111
* `await_ready` - this function is called before the coroutine suspension. If it returns `false` the coroutine will be suspended. If it returns `true` it will ignore the suspension.
108112
> Note: You can also write `void await_ready() {}` and it will behave the same as if it return `false`.
109113
* `await_suspend` - this function is called right after the suspension. It contains the actual instance of the handle of the suspended coroutine which might be stored and used later.
@@ -119,19 +123,20 @@ The Promise structure contains a configuration of the coroutine, which defines h
119123
* `suspend_always` - the function will be suspended at the end. You must call `destroy()` function on the handle manually. Only with this setting you can check if the coroutine has finished by using the `done()` function on the handle.
120124
> Note: `initial_suspend` and `final_suspend` are one of the most important functions here, as they can control when the coroutine can be safely destroyed. We will utilize these functions later in more advanced coroutines.
121125
* `return_void` - this function is called when the `co_return` is used. The `co_return` is called by default when the function reaches the end.
126+
> Note: A Promise can also implement `return_value`. How to implement it is described **[here](#co_return-value)**.
122127
* `unhandled_exception` - used to catch any exception. To get more details about the catched extepction use `std::current_exception()` function.
123128

124129
## Using the coroutine
125130
Ok, we have a very simple coroutine defined, but how do we use it? In our example we have a function, which returns our coroutine handle, the `CoroHandle`. It uses `co_await` keyword to suspend it's execution. In the `main` program we call this function and gets the `handle` to it. Later, we can use the `resume()` function on that handle to resume the suspended coroutine. And that's all!
126131

127132
[Back to index](#index)
128133

129-
# Coroutine Tasks
130-
Coroutine Tasks are the elegant way to define different coroutine Handles using the same Handle base and Promise, reducing the amount of boilerplate code you need to write.
134+
# Awaiters
135+
Awaiters are the elegant way to define different awaiting logic using the same Handle and Promise, reducing the amount of boilerplate code you need to write.
131136

132-
The code below implements two different tasks: `CoroTaskA` and `CoroTaskB`. They can be called when using `co_await` in the same coroutine which returns the same type of Handle.
137+
The code below implements two different awaiters: `AwaiterA` and `AwaiterB`. They can be called when using `co_await` in the same coroutine which returns the same type of Handle.
133138

134-
Tasks are defined using the class instead of structs, so they allow better code encapsulation.
139+
> Note: You can notice that in the previous example the Handle was also the Awaiter.
135140
136141
This code with comments is also inside the `Samples` directory here: [02_CoroTasks.cpp](Samples/02_CoroTasksExample.cpp)
137142

@@ -154,37 +159,37 @@ struct CoroPromise
154159
void unhandled_exception() {}
155160
};
156161

157-
class CoroTaskBase
162+
class AwaiterBase
158163
{
159164
public:
160165
void await_resume() {}
161166
bool await_ready() { return false; }
162167
};
163168

164-
class CoroTaskA : public CoroTaskBase
169+
class AwaiterA : public AwaiterBase
165170
{
166171
public:
167172
void await_suspend(std::coroutine_handle<CoroPromise> Handle)
168173
{
169-
std::cout << "Suspended Using Task A\n";
174+
std::cout << "Suspended Using Awaiter A\n";
170175
};
171176
};
172177

173-
class CoroTaskB : public CoroTaskBase
178+
class AwaiterB : public AwaiterBase
174179
{
175180
public:
176181
void await_suspend(std::coroutine_handle<CoroPromise> Handle)
177182
{
178-
std::cout << "Suspended Using Task B\n";
183+
std::cout << "Suspended Using Awaiter B\n";
179184
};
180185
};
181186

182187
CoroHandle CoroTest()
183188
{
184189
std::cout << "CoroTest Before Suspend\n";
185-
co_await CoroTaskA();
190+
co_await AwaiterA();
186191
std::cout << "CoroTest After First Resume\n";
187-
co_await CoroTaskB();
192+
co_await AwaiterB();
188193
std::cout << "CoroTest After Second Resume\n";
189194
}
190195

@@ -203,24 +208,21 @@ The output of this code will be:
203208

204209
```
205210
CoroTest Before Suspend
206-
Suspended Using Task A
211+
Suspended Using Awaiter A
207212
CoroTest First Resuming
208213
CoroTest After First Resume
209-
Suspended Using Task B
214+
Suspended Using Awaiter B
210215
CoroTest Second Resuming
211216
CoroTest After Second Resume
212217
```
213218

214-
The Promise definition is the same as previously. The coroutine Handle has been split into two parts:
215-
* the definition of the coroutine Handle which uses the defined Promise
216-
* the definition of Tasks which implements the Handle functions such as `await_resume`, `await_ready` and `await_suspend`.
217-
218-
As you can see in this example we can call different Tasks inside the same coroutine function.
219+
As you can see in this example we can call different Awaiters inside the same coroutine function.
219220

220221
[Back to index](#index)
221222

222223
# Generators - coroutines returning values
223-
Just suspending a running function is neat, but we can do more. Coroutines can be written in such way that they can store values which can be used later. Such coroutines are called Generators. Let's write a generic Generator and then use it to write a coroutine which will give us every value of the beloved Fibonacci sequence one by one.
224+
Just suspending a running function is neat, but we can do more. Generators are a special type of Awaiters which, when suspended, can store a value. This value can be used later.
225+
Let's write a generic Generator and then use it to write some simple counting coroutine.
224226

225227
This code with comments is also inside the `Samples` directory here: [03_CoroGenerators.cpp](Samples/03_CoroGenerators.cpp)
226228

@@ -271,6 +273,62 @@ struct CoroGenerator
271273
~CoroGenerator() { Handle.destroy(); }
272274
};
273275

276+
CoroGenerator<int> CountToThree()
277+
{
278+
co_yield 1;
279+
co_yield 2;
280+
co_yield 3;
281+
}
282+
283+
int main()
284+
{
285+
auto generator = CountToThree();
286+
while (generator)
287+
{
288+
std::cout << generator() << " ";
289+
}
290+
return 0;
291+
}
292+
```
293+
294+
The output of this code will be:
295+
296+
```
297+
1 2 3
298+
```
299+
300+
Once again, there is a lot to cover. Let's go through this step by step.
301+
302+
## Coroutine Promise for Generator
303+
The Promise for a Generator is slightly different. You can notice few differences:
304+
* `T Value` - Promise stores a value of a generic type. This value can be obtained later by a Generator.
305+
* `get_return_object` - doesn't return coroutine Handle, but a Generator with a coroutine Handle passed as an argument to the constructor.
306+
* `initial_suspend` and `final_suspend` returns `suspend_always`. This is important, because with such setup we have the full control over the coroutine flow.
307+
* `yield_value` - this function is called every time when `co_yield` is used. It stores the given value to the `Value` variable and returns `suspend_always` in order to suspend the function.
308+
309+
The Promise is defined inside the Generator struct in order to keep everything in one place and to avoid declaration loop.
310+
311+
## Generator
312+
The generator itself has few interesting parts as well:
313+
* `Handle` - this is the coroutine Handle saved from the Generator constructor.
314+
* `operator bool()` - is very handy for resuming the coroutine and checking if the coroutine has finished. To check if the coroutine is done we use `done()` function on the coroutine Handle. We can use it safely, because the `final_suspend` is set to `suspend_always`, so the coroutine Handle will not be destroyed automatically when the function is finished.
315+
* `operator()` - will be used to get a stored value from the Promise.
316+
* Constructor - receives and remembers the coroutine Handle. The Generator construcor is used in `get_return_object` function in the Promise.
317+
* Destructor - explicitly destroys the coroutine Handle using `destroy()` function. It must be used, because the `final_suspend` is set to `suspend_always`, so it won't be destroyed automatically.
318+
319+
## The Counter
320+
`CountToThree` coroutine returns the Generator which stores the `int` value. Every `co_yield` stores the given value inside of the Generator, which can be obtained later.
321+
322+
## Using the Counter
323+
When Generator is constructed it automatically suspends, because of the `initial_suspend` set to `suspend_always`. Inside the while loop the `operator bool()` override is used to resume the Generator and check if the coroutine has already finished. Inside the while loop the `operator()` override is used to get lastly yielded value.
324+
325+
[Back to index](#index)
326+
327+
# Fibonacci Generator
328+
329+
We can write a slightly more advanced Generator. This one will yield every next number in the Fibonacci sequence.
330+
331+
```c++
274332
CoroGenerator<int> FibonacciGenerator(const int Amount)
275333
{
276334
if (Amount <= 0)
@@ -313,30 +371,38 @@ The output of this code will be:
313371
1 1 2 3 5 8 13 21 34 55
314372
```
315373

316-
Once again, there is a lot to cover. Let's go through this step by step.
374+
[Back to index](#index)
317375

318-
## Coroutine Promise for Generator
319-
The Promise for a Generator is slightly different. You can notice few differences:
320-
* `T Value` - Promise stores a value of a generic type. This value can be obtained later by a Generator.
321-
* `get_return_object` - doesn't return coroutine Handle, but a Generator with a coroutine Handle passed as an argument to the constructor.
322-
* `initial_suspend` and `final_suspend` returns `suspend_always`. This is important, because with such setup we have the full control over the coroutine flow.
323-
* `yield_value` - this function is called every time when `co_yield` is used. It stores the given value to the `Value` variable and returns `suspend_always` in order to suspend the function.
376+
# co_return value
377+
A Coroutine Promise can also define `return_value` function, which will be called when `co_return` with a value is used. `return_value` and `return_void` can't be defined at the same time, only one of these functions can be used in the same Promise.
324378

325-
The Promise is defined inside the Generator struct in order to keep everything in one place and to avoid declaration loop.
379+
## co_return value in Awaiter
380+
In Awaiter the `return_value` must be defined with the value type it can return. For `int` it will be:
381+
```cpp
382+
int ReturnValue;
383+
void return_value(int Value)
384+
{
385+
ReturnValue = Value;
386+
}
387+
```
326388
327-
## Generator
328-
The generator itself has few interesting parts as well:
329-
* `Handle` - this is the coroutine Handle saved from the Generator constructor.
330-
* `operator bool()` - is very handy for resuming the coroutine and checking if the coroutine has finished. To check if the coroutine is done we use `done()` function on the coroutine Handle. We can use it safely, because the `final_suspend` is set to `suspend_always`, so the coroutine Handle will not be destroyed automatically when the function is finished.
331-
* `operator()` - will be used to get a stored value from the Promise.
332-
* Constructor - receives and remembers the coroutine Handle. The Generator construcor is used in `get_return_object` function in the Promise.
333-
* Destructor - explicitly destroys the coroutine Handle using `destroy()` function. It must be used, because the `final_suspend` is set to `suspend_always`, so it won't be destroyed automatically.
389+
To get this value, get a Promise from the Handle and then get this value
390+
```cpp
391+
std::cout << "Returned Value: " << handle.promise().ReturnValue << "\n";
392+
```
334393

335-
## Fibonacci Generator
336-
`FibonacciGenerator` function returns the Generator which stores the `int` value. Every next value of the Fibonacci sequence is yielded, which means the function is suspended and the value is stored.
394+
Important! Remember, that the `final_suspend()` of this awaiter must return `std::suspend_always`, otherwise the Handle after `co_return` will not be valid!
337395

338-
## Using Fibonacci Generator
339-
When our Generator is constructed it automatically suspends, because of the `initial_suspend` set to `suspend_always`. Inside the while loop the `operator bool()` override is used to resume the Generator and check if the coroutine has already finished. Inside the while loop the `operator()` override is used to get lastly yielded value.
396+
## co_return value in Generator
397+
In Generator defining `return_value` is very similar to defining `yield_value`.
398+
```cpp
399+
template<std::convertible_to<T> From>
400+
void return_value(From&& from)
401+
{
402+
Value = std::forward<From>(from);
403+
}
404+
```
405+
The difference between `return_value` and `yield_value` is that `yield_value` will suspend the coroutine, while `return_value` will finish it.
340406
341407
[Back to index](#index)
342408
@@ -394,17 +460,16 @@ public:
394460
395461
CoroHandle CoroFadeOut()
396462
{
397-
if (GWorld)
463+
for (int32 Fade = 0; Fade <= 100; Fade += 10)
398464
{
399-
APlayerCameraManager* CameraManager = UGameplayStatics::GetPlayerCameraManager(GWorld, 0);
400-
for (int32 Fade = 0; Fade <= 100; Fade += 10)
465+
if (GWorld)
401466
{
402-
if (IsValid(CameraManager))
467+
if (APlayerCameraManager* CameraManager = UGameplayStatics::GetPlayerCameraManager(GWorld, 0))
403468
{
404469
CameraManager->SetManualCameraFade((float)Fade * .01f, FColor::Black, false);
405470
}
406-
co_await WaitSecondsTask(.1f);
407471
}
472+
co_await WaitSecondsTask(.1f);
408473
}
409474
}
410475
```
@@ -422,7 +487,24 @@ The fade out function changes the camera fade in a for loop in 10 steps every 0.
422487

423488
In order to use this function, simply call it in your project.
424489

425-
## More coroutines for UE5
490+
[Back to index](#index)
491+
492+
# More coroutines for UE5
426493
If you are interested witch coroutines implementation for Unreal Engine 5 check out this amazing plugin [UE5Coro](https://github.com/landelare/ue5coro). Coroutines are also implemented in my UE plugin [Enhanced Code Flow](https://github.com/zompi2/UE4EnhancedCodeFlow) as well :)
427494

495+
[Back to index](#index)
496+
497+
# Stability concerns
498+
499+
When using coroutines it is important to be aware that the coroutine doesn't track the lifetime of it's owner.
500+
501+
If the owner of the coroutine has been destroyed while the coroutine is suspended, when something resumes the coroutine we will end up in an invalid place in memory.
502+
503+
The solution for game engines like Unreal Engine is to keep a soft reference to the owner which calls the coroutine inside the Awaiter and simply do not resume the coroutine if the reference becomes invalid.
504+
505+
For pure c++ solutions it would require some sort of objects lifetime tracker.
506+
507+
There is no bad side effect of keeping coroutines suspended forever, because they are stackless and all data required for their resume is stored inside a Handle, which is destroyed when going out of scope.
508+
509+
428510
[Back to index](#index)

0 commit comments

Comments
 (0)