Mikelis' Game Blog

Functions as Data in Unreal Engine 5

Three ways to handle functions as any other variable in Unreal Engine 5.

Kazimieras Mikelis' Game Blog

At times, we need to store references, pointers, or copies of functions for future execution. This can be helpful when using them as callbacks or offering a generic way to include arbitrary code within our algorithms.

In this article, we'll examine three methods for achieving this in Unreal Engine:

  1. using C++ function pointers,
  2. creating a delegate that holds a reference or copy of a function,
  3. and employing TFunction<>.

We will also look into building something similar to C++ lambda functions using delegate function arguments in blueprints. We won't delve into std::function<> or other features that require an excursion into the standard function library — all three ways to handle functions as data will be Unreal-native, and can be seen used in Unreal Engine's source code.

Heads up — this article is very technical and might involve a bit of a learning journey if you are not familiar with C++ templates.

Using C++ Function Pointers

This is a basic C++ way of pointing to functions that can be executed when needed. Aside from the unappealing syntax, this is just a pointer.

// A pointer to a function with two int32 arguments returning a bool:
bool(*VariableName)(int32, int32) = nullptr;

// An assignment of a static function after declaration:
VariableName = &UMyClass::MyFunction;

// Function call using the variable:
int32 ExampleInt = 1;
bool RetVal = (*VariableName)(ExampleInt, ExampleInt * 2);

// Or alternatively:
VariableName(ExampleInt, ExampleInt * 2);

// You can also easily use this type as an argument in a function, here's a prototype:
void SomeOtherFunction(bool(*ArgumentName)(int32, int32), int32 AnotherArgument); // Quite obviously, this is not supported by UHT — this function cannot be exposed to blueprints.

The above works well for static functions, but doesn't seem to have a way to call non-static member functions in an object. For this, the variable name syntax is even more unusual.

// A pointer pointing to a member function:
bool (UMyClass::*VariableName)(int32, int32) = nullptr;

// An assignment of the function after declaration:
VariableName = &UMyClass::MyFunction;

// A function call using this variable:
UMyClass* SomeInstanceOfMyClass = //..
(SomeInstanceOfMyClass->*VariableName)(1, 2);

// We can also use this as a function argument:
template<typename T>
void SomeOtherFunction(T* Object, bool(T::*ArgumentName)(int32, int32), int32 AnotherArgument)
{
    (Object->*ArgumentName)(AnotherArgument, AnotherArgument * 2);
}

This is very useful if we need to write an algorithm we want to wrap our code. Here's an even more generalized example which takes any function signature and runs it in a thread-safe critical section:

/** A RAII-style scoped critical section for the stack. */
struct FRunThreadSafeCriticalSection
{
    FRunThreadSafeCriticalSection()
    {
    	// Locks the critical section.
    	GetSectionInstance().Lock();
    }

    ~FRunThreadSafeCriticalSection()
    {
    	// Always unlocks the critical section when FRunThreadSafeCriticalSection is deallocated on stack.
    	GetSectionInstance().Unlock();
    }

    /* Creates a critical section singleton and gets it. */
    static FCriticalSection& GetSectionInstance()
    {
    	static FCriticalSection SectionInstance;
    	return SectionInstance;
    }

private:
  // Not made for the heap.
    void* operator new(size_t);
    void* operator new[](size_t);
};

/** Runs a lambda in a thread-safe way. */
template<typename T, typename F, typename... TArgs>
static void RunThreadSafe(T* Object, F Function, TArgs... Args)
{
    FRunThreadSafeCriticalSection ScopedCriticalSection;

    (Object->*Function)(Forward<TArgs>(Args)...);
}

However, if you try to use it, you will soon notice that to cache the function pointer, you need two pieces of data — the object instance to run it on, and the function pointer in the appropriate class. You could create a type to hold these two pieces of information, like this:

/** An example function pointer capable of invoking a function on a UObject-derived class instance. */
template<typename ObjType, typename RetType, typename... Args>
struct TFunctionPtr
{
    using MemberFunctionPtr = RetType (ObjType::*)(Args...);

    TFunctionPtr(ObjType* InObject, MemberFunctionPtr InFunction) : Object(InObject), bIsUObject(Cast<UObject>(InObject) != nullptr), Function(InFunction) {}

    /** Call the pointed-to function with its arguments.*/
    RetType Invoke(Args... Arguments)
    {
    	if(!ensureAlwaysMsgf(IsValid(), TEXT("Attempting to invoke a function by a non-valid pointer.")))
    	{
    		return RetType();
    	}

    	// Some code like scoped critical section stuff could go here if needed.
    	
    	return (Object->*Function)(Forward<Args>(Arguments)...);
    }

    /** Checks whether the pointer is initialized and has not gone stale (only for UObjects). */
    bool IsValid()
    {
    	return Object != nullptr && (!bIsUObject || IsValidObject(Object));
    }

private:
    ObjType* Object = nullptr;
    bool bIsUObject = false;
    MemberFunctionPtr Function = nullptr;
};

Which could then be used as follows:

// Assuming that the function bool AMyClass::MyFunction(int32, int32) exists and this is an instance of AMyClass:
TFunctionPtr MyFunction(this, &AMyClass::MyFunction);
bool RetVal = Pointer.Invoke(42, 42);

Yet, we don't really need to write anything like this ourselves, because with small additional overhead, Unreal Engine delegates can work as even more flexible function pointers (or even lambda function containers).

C++ Delegates

Delegates in Unreal Engine are just a generic way to call arbitrary functions (both plain old C++ functions, lambda functions, and of course, blueprint-implemented functions). Fundamentally, they hold nullable associations with functions, similar to pointers. Although delegates can also hold copies of lambdas.

To best explain how delegates can be used as data types pointing to functions, let's see two examples — one more verbose and one shorter.

// Verbose example. Assume a function with signature UMyClass::DoSomethingElse(const FString&) exists.

void UMyClass::MySampleFunction(const FString& MyString)
{
    // A simple function to log a string at Display verbosity.
    auto PrintStringToLog = [](const FString& String)
    {
        GLog->Log(ELogVerbosity::Display, *String);
    };

    // This is the important part.
    // First, we declare a new delegate type. It's just a struct, and can be declared in any scope.
    DECLARE_DELEGATE_OneParam(FMyDelegate, const FString&);

    // Now we instantiate a new delegate. This could also be in any scope. For example, it could be a member variable UMyClass::MyDelegateMember;
    FMyDelegate MyDelegate;

    // Now we associate this delegate with a function at random.
    if(FMath::RandBool())
    {
        MyDelegate.BindLambda(PrintStringToLog);
    }
    else
    {
        MyDelegate.BindUObject(this, &UMyClass::DoSomethingElse);
    }

    // Finally, we call the associated function.
    MyDelegate.Execute(MyString);
}

Now let's look at a shortened example, closer to what might be found in the engine:

// Short example. Assume a function with signature UMyClass::DoSomethingElse(const FString&) exists.

void UMyClass::MySampleFunction(const FString& MyString)
{
    DECLARE_DELEGATE_OneParam(FMyDelegate, const FString&);

    (FMath::RandBool() 
     ? FMyDelegate::CreateLambda([](const FString& String)
     {
         GLog->Log(ELogVerbosity::Display, String);
     })
     : FMyDelegate::CreateUObject(this, &UMyClass::DoSomethingElse)).Execute(MyString);
}

Naturally, many delegates associated with different functions of the same signature can be created, and they can be stored for later as needed, making delegates an ideal option for callbacks.

However, the best thing about this approach is that delegates are a partially-supported data type in blueprints, and we can use them as function arguments.

Kazimieras Mikelis' Game Blog

Now the visual programmers can pass a function reference as a function argument, it is no longer limited to C++. An example RunFunction implementation is here:

// -- Class header --

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyClass.generated.h"

// Notice that here we declare the delegate in the global scope. It could also be declared in the class scope.
DECLARE_DYNAMIC_DELEGATE_OneParam(FMyFunc, const FString&, String);

UCLASS(Blueprintable, BlueprintType)
class MYPROJECT_API AMyClass : public AActor
{
    GENERATED_BODY()
    
public:

    /** Runs a function by delegate. */
    UFUNCTION(BlueprintCallable)
    void RunFunction(FMyFunc Function, const FString& String);
};

// -- Class body --

#include "MyClass.h"

void AMyClass::RunFunction(FMyFunc Function, const FString& String)
{
    if(Function.IsBound())
    {
        // Do something here...

    	Function.Execute(String);

        // ... or here.
    }
}

Delegates offer a fantastic way to reference functions, with several types to choose from:

Delegates can be used in both C++ and blueprints (where they're known as event dispatchers), and some are even serializable. Blueprint functions can not only be bound to delegates, but they can also be exposed to blueprints for binding and calling if their structs (e.g., FMyDelegate) are reflected member variables decorated with UPROPERTY(BlueprintAssignable, BlueprintCallable). This makes for an incredibly versatile function reference.

However, delegate support as data types in blueprint graphs is somewhat limited. For instance, they can't be stored in local (graph), function, or blueprint variables. Replicating the graph shown above with the K2 Select node might require multiple attempts due to incomplete implementation.

Despite these technical challenges, this enables lambda-like function declaration and use in Blueprints, which is incredibly beneficial.

For more information on delegates, consult the Unreal Engine documentation: Delegates | Unreal Engine 4.27 Documentation.

TFunction<>

Finally, TFunction is a general purpose function wrapper and container (though not in the traditional container-of-elements sense). It is the Unreal Engine's equivalent to std::function<>.

There isn't much a TFunction<> can do that a delegate can't, and I would guess it was added to Unreal Engine to help those familiar with std::function<> but not delegates. While delegates can do all that TFunction<> can, TFunction<> is not as versatile as delegates. Particularly, TFunction<> and its derived types can't be used in or reflected to the blueprint VM.

Here is some sample TFunction<> code:

// Declaring a variable:
TFunction<bool(int32, int32)> FunctionTakingTwoIntsAndReturningABool = nullptr;

// Assigning a pointer to a static function:
const TFunction<void()> StaticFunctionReference = &AMyClass::MyStaticFunction;

// Assigning a "pointer" to a non-static member function (done via a lambda):
const TFunction<void()> NonStaticFunctionReference = [WeakThis = TWeakObjectPtr<AMyClass>(this)]()
{
    if(WeakThis.IsValid())
    {
    	WeakThis->MyNonStaticFunction();
    }
};

// Assigning a lambda function:
const TFunction<void()> LambdaFunction= [](){};

// Examples of invoking TFunction<>:
StaticFunctionReference();

const TFunction<void()>& SelectedFunction = FMath::RandBool() ? NonStaticFunctionReference : LambdaFunction;
SelectedFunction();

(FMath::RandBool() ? NonStaticFunctionReference : LambdaFunction).operator()();

// Using TFunction<> type as an argument in a function prototype:
void MyFunction(TFunction<bool(int32, int32)>& Argument);

It's very basic, and it has a lower overhead and complexity than delegates. This makes TFunction<> a common choice for passing functions as arguments in the engine's C++ code.

Conclusion

In this article, we've explored three ways to treat functions as data types in Unreal Engine 5. We began with plain C++ function pointers, which are still used in the engine's source. Then, we discussed delegates — often the preferred method for representing functions as data in Unreal Engine due to their flexibility and partial blueprint support. Finally, we examined TFunction<> as an alternative to std::function<> in Unreal Engine.

I hope you found this article informative and gained valuable insights!

#Blueprints #Cpp #Engineering #Game Development #Unreal Engine