Using co_await and TTask for asynchronous programming in C++
To simplify asynchronous programming in C++, the Redpoint EOS Online Framework provides TTask<> via the RedpointEOSAsync module. Many of the newer C++ APIs return a TTask<> so that you can use co_await.
TTask<> is analogous to the await/async support that exists in C#, and is designed to be very similar in behaviour.
The C++ APIs described in this section are only available in the Paid Edition, as the Free Edition does not ship with any C++ headers.
Updating your game module dependencies
Before you can access the asynchronous types, you need to update your game module's .Build.cs file to depend on the RedpointEOSAsync module, like so:
PrivateDependencyModuleNames.AddRange(new string[]
{
"RedpointEOSAsync",
});
For all of these APIs, you will need to include the correct headers and the appropriate namespace:
#include "RedpointEOSAsync/Task.h"
using namespace ::Redpoint::EOS::Async;
Awaiting a task
When a C++ function returns TTask<>, you can use co_await to asynchronously wait for the result. You can only use co_await if the function you are using it in also returns TTask<>:
// Assuming Hello() is declared as:
//
// TTask<int32> Hello();
int32 Result = co_await Hello();
Handling a task as a callback
If you are writing a function that is not returning TTask<>, and you want to handle the result of a TTask<> as a callback, you can use the AsCallback function:
// Assuming Hello() is declared as:
//
// TTask<int32> Hello();
AsCallback(
Hello(),
[](int32 Result)
{
// Use 'Result' on potentially a different frame.
});
Creating a deferred task
If you want to return a TTask<> from a function, but need to interact with code that uses callbacks, you can create a "deferred task" and return it. A deferred task is one that promises to have a result at a later point in time.
For example, the implementation of Delay looks like this:
TTask<void> Delay(float Seconds)
{
auto Deferred = TTask<void>::Deferred();
FTSTicker::GetCoreTicker().AddTicker(
FTickerDelegate::CreateLambda([Deferred](float) -> bool {
Deferred.SetValue();
return false;
}),
Seconds);
return Deferred;
}
With this function, callers can use co_await to wait some amount of time:
co_await Delay(1.0f); // Wait 1 second.
Writing asynchronous functions with TTask<>
To write asynchronous functions, your function needs to return a TTask<>. If you don't have a return value (where you would use void), then the return type should be TTask<void>. Otherwise, you would use TTask<TypeToReturn>.
There are some limitations as to when you can use TTask<> as a return value, and this is because the lifetime of memory in C++ is manually managed.
You can use TTask<> directly when your function is declared on a class that inherits from UObject, TSharedFromThis<>, or returns a deferred task (and doesn't call co_await):
UCLASS()
class AMyActor : public AActor
{
// ...
public:
TTask<int32> MyAsyncFunction(FString Hello);
};
TTask<int32> AMyActor::MyAsyncFunction(FString Hello)
{
// Safe to use co_await.
co_return (co_await SomeOtherFunction());
}
class FMySharedObject : public TSharedFromThis<FMySharedObject>
{
public:
TTask<int32> AnotherAsyncFunction(FString Hello);
};
TTask<int32> FMySharedObject::AnotherAsyncFunction(FString Hello)
{
// Safe to use co_await.
co_return (co_await SomeOtherFunction());
}
In the cases above, if AMyActor is garbage collected, or FMySharedObject is released, then the asynchronous functions will not continue past the co_await. Effectively, if the object the asynchronous function is associated with is released, the asynchronous functions stop running the next time they are able and will never return. Any code that is co_awaiting a task returned by a released object will never continue either.
All functions that use co_await must pass parameters by value. You can not pass by const&, as only the reference will be captured for the asynchronous stack frame, and not the value. You will receive a compiler error if you try to use co_await in a function with a const& parameter.
All functions using TTask<>, even those using TTask<void>, must call co_return when returning from the function. This is required for the compiler to generate correct return code.
You can also use co_await in static functions, but you need to indicate the function is static by using ::With<ETaskBinding::Static>, like so:
TTask<void>::With<ETaskBinding::Static> MyFunction(float Seconds)
{
// Wait some time using Delay.
co_await Delay(Seconds);
// Print out a message.
UE_LOG(LogTemp, Warning, TEXT("Waiting %f seconds."), Seconds);
// co_return - always required, even in TTask<void> functions.
co_return;
}
You can also use co_await in lambda functions, but the lambda functions must not have any captures, and you need to indicate the function is a lambda using ::With<ETaskBinding::Lambda>:
auto MyLambda = [](float Seconds) -> TTask<void>::With<ETaskBinding::Lambda>
{
// Wait some time using Delay.
co_await Delay(Seconds);
// Print out a message.
UE_LOG(LogTemp, Warning, TEXT("Waiting %f seconds."), Seconds);
// co_return - always required, even in TTask<void> functions.
co_return;
};
AsCallback(
MyLambda(),
[]()
{
// Optionally, do something after the lambda finishes.
});
Using lambdas with TTask<> is not recommended, since we can't automatically stop the asynchronous work when associated memory is released. It's always better to declare the function on a UObject or TSharedFromThis.