Depends on what you want to do, I suppose?
Right now, I find myself using the task graph. I define a task to do the work I need, I queue it into the graph, and I tie an event-handler to the completion event.
To let me do this in a generic manner, I have a templated FLambdaTask I created, where I basically define a C++ lambda, pass the lambda into the task, and toss the task into the task graph. As an example (and forgive any mistakes, since I’m writing this right here in a thread posting box rather than, y’know, in a compiler):
// Assume we have an FRocketCalculationJob structure which contains
// the information we want to use in this calculation.
//
// There is also an FLambdaTaskDelegate defined, which takes an
// FLambdaTask::Result, which is mostly just an FJsonObject (to allow receiving
// arbitrary results), along with a unique task ID.
//
FRocketCalculationJob Job();
Job.Rocket = MakeShareable(SomeRocket);
Job.Fuel = CurrentFuel;
Job.Destination = EPlanets::Neptune;
// QueueTask queues a task, and returns an FLambdaTask::TaskInfo.
auto TaskInfo = FLambdaTask::QueueTask<FRocketCalculationJob>(Job,
[] (FLambdaTask::State<FRocketCalculationJob>* TaskState) {
// This lambda is basically the coroutine body.
// Get back the Job we passed in, of type FRocketCalculationJob.
auto JobData = TaskState->GetJobData();
// Do something I didn't want to do in that main function...
bool IsViable = UAerospaceMathLibrary::IsDestinationViable(JobData.Destination,
JobData.Rocket, JobData.Fuel);
// Got back our answer. Let's add it to the result.
TaskState->Result.AddBoolValue("Viable", IsViable);
// We exit the lambda. The Task's DoWork then executes the delegate,
// if it's bound, passing the FLambdaResult
});
// FLambdaTask::TaskInfo contains a 'Completed' field, which is
// an FLambdaTaskDelegate that I can bind to. Let's do that for
// the one we just set up...
//
// It also contains a unique task ID, so if I was queuing multiple
// jobs bound to the same incoming function, I could store contextual
// data based on the task ID, and then look up the contextual data
// when I got the result.
TaskInfo.Completed.Bind(this, &UExampleClass::OnViabilityCalculationComplete);
I don’t know if that’s best; truthfully, it’s probably not, because I half-suspect I did something horrifying with the delegate behind the scenes that’s going to bite me someday. And since some of this was written in a fit of insomniac pique, I honestly don’t remember why the task state is a pointer, but…
¯\_(ツ)_/¯