Raw Source Code

Unreal delegates under the hood - Part II

More insight into the general structure and architecture of Unreal Delegate System internals.

Previously we dived into Unreal TDelegate class by writing a simple and incomplete minimalist clone of Unreal Unicast Delegate.

Today we aim to expand previous knowledge of TDelegates with some additional graphics and insightful info for anyone interested in diving deeper into the inner workings of Delegates.

For a comprehensive and introductory explanation of different kinds of delegates present in Unreal, please check this nice article from BenUI.


What is class TDelegate?

Roughly speaking It’s the templated class that holds a pointer to something that you can invoke i.e. a callable type like a lambda expression, an object method, a free function, etc..

A TDelegate uses a helper object that implements the interface IDelegateInstance to store this data.

Figure1: TDelegate Basic Structure

Figure1: TDelegate Basic Structure


TDelegate default policy

In Generic Programming a policy allows you to configure the behavior of a specific generic type at compile time. Please check the following references for more information.

A policy is a class or class template that defines an interface as a service to other classes.

Computer Science Presentation at Karlsruhe Institute of Technology

A policy is a generic function or class whose behavior can be configured [... at compile time]

Rainer Grimm at modernescpp.org

We find that TDelegate can be configured using a default policy named FDefaultDelegateUserPolicy which defines the following type aliases. (We simple users don't care about this, right?).

FDelegateInstanceExtras

using FDelegateInstanceExtras = IDelegateInstance;

A type alias to a class that publicly inherits the interface IDelegateInstance, classes that implement this interface are responsible for holding any relevant data necessary to invoke a delegate e.g. pointer to objects, function pointers, function names etc.

FDelegateExtras

using FDelegateExtras = TDelegateBase<FThreadSafetyMode>;

A type alias to a class that publicly inherits the class TDelegateBase and holds an instance of a class that derives IDelegateInstance.

FMulticastDelegateExtras

Similar to FDelegateExtra but for Multicast Delegates, let’s ignore for now.

FThreadSafetyMode

Used to synchronize the access to delegate internal data in a multithreaded environment, let’s ignore for now.

This policy is used mainly to configure the inheritance relationship of classes shown previously at Figure 1

Figure1: TDelegate Inheritance diagram overview

Figure2: TDelegate Inheritance diagram overview

For the default user policy this translates to TDelegate -> extends -> TDelegateBase -> owns/composes -> IBaseDelegateInstance -> extends -> IDelegateInstance.

IDelegateInstance vs IBaseDelegateInstance

IDelegateInstance defines the interface to fetch useful info like the UObject a delegate instance is bound to, a pointer to the associated method pointer or a bool indicating safety of executing the delegate instance, on the other hand IBaseDelegateInstance describes the interface to execute a delegate instance

IDelegateInstanceInterfaceIBaseDelegateInstanceInterface

Jargon challenge

Typically member functions in UserPolicy::FDelegateExtras (e.g. TDelegateBase) and its derived classes will forward calls to the inner UserPolicy::FDelegateInstanceExtras class (e.g. IDelegateInstance) by downcasting the result of a call to TDelegateBase::GetDelegateInstanceProtected().

For example check how TDelegate (which extends from TDelegateBase) downcasts its owned IDelegateInstance to IBaseDelegateInstance in order to execute the delegate.

/**
* Execute the delegate.
*
* If the function pointer is not valid,
* an error will occur.
* Check IsBound() before
* calling this method or
* use ExecuteIfBound() instead.
*
* @see ExecuteIfBound
*/
FORCEINLINE RetValType
Execute(ParamTypes... Params) const
{
  FReadAccessScope ReadScope = GetReadAccessScope();

  // The following method down-casts from 
  // IDelegateInstance to IBaseDelegateInstance

  const DelegateInstanceInterfaceType*
  LocalDelegateInstance = GetDelegateInstanceProtected();

  // Where DelegateInstanceInterfaceType is:
  // using DelegateInstanceInterfaceType = typedef IBaseDelegateInstance/*...*/;

  // If this assert goes off, Execute() was called before a function was bound to the delegate.
  // Consider using ExecuteIfBound() instead.
  checkSlow(LocalDelegateInstance != nullptr);

  // Use the interface to invoke the associated callable
  return LocalDelegateInstance->Execute(Forward<ParamTypes>(Params)...);
}

Concrete IBaseDelegateInstance implementations:

class TBaseStaticDelegateInstance

Holds a pointer to free function or static member function

TBaseStaticDelegateInstance

class TBaseRawMethodDelegateInstance

Holds a pointer to a raw C++ object (instance of class that doesn’t have the UOBJECT declaration) and a pointer to a member function in the object.

TBaseRawMethodDelegateInstance

class TBaseFunctorDelegateInstance

Holds a functor object.

TBaseFunctorDelegateInstance

class TWeakBaseFunctorDelegateInstance

Holds a functor object and a weak object pointer that will be used to define the safety of invoking the associated functor object.

TWeakBaseFunctorDelegateInstance

class TBaseSPMethodDelegateInstance

Holds a weak pointer to a raw C++ object or class derived from TSharedFromThis and a pointer to a member function in the object.

TBaseSPMethodDelegateInstance

class TBaseUFunctionDelegateInstance

Holds a weak object pointer and an FName of a function declared with UFUNCTION, also stores a cache to the associated UFunction.

TBaseUFunctionDelegateInstance

class TBaseUObjectMethodDelegateInstance

Holds a weak object pointer and a pointer to a member function in the UObject.

TBaseUObjectMethodDelegateInstance

How to create distinct delegate instances?

TDelegate exposes the public interface of the Unicast Delegate API and exposes two kind of functions to instance concrete IBaseDelegateInstance objects (described above), the first one are static functions with prefix Create, these are used to instance/construct a TDelegate object and bind the callable (e.g. function pointer, functor, etc..) into the inner IDelegateInstance.

The second family uses a prefix Bind on the function name, aren’t static and are used to change the callable bound to a TDelegate object or to bind a callable into TDelegate later in user code (very useful when you write a function that receives a default callback delegate).

So in summary:

Create prefixBind prefix
Static function member.Non static function member.
Instances a TDelegate and binds a callable into it.Binds a callable into an existing TDelegate object.
Can be used to instance all concrete IBaseDelegateInstance classes.Can be used to instance all concrete IBaseDelegateInstance classes.

All the Create/Bind functions are:

Create prefixBind prefixConcrete IBaseDelegateInstance
CreateStaticBindStaticTBaseStaticDelegateInstance
CreateLambdaBindLambdaTBaseFunctorDelegateInstance
CreateWeakLambdaBindWeakLambdaTWeakBaseFunctorDelegateInstance
CreateRawBindRawTBaseRawMethodDelegateInstance
CreateSPBindSPTBaseSPMethodDelegateInstance
CreateUFunctionBindUFunctionTBaseUFunctionDelegateInstance
CreateUObjectBindUObjectTBaseUObjectMethodDelegateInstance

Final comments:

With this we finish our reduced study on implementation details of TDelegate public API, next we’ll start taking a look into the payload system, thread safety and memory management of these objects and will eventually expand into the Multicast Delegates which in essence are only a plain array of TDelegate objects.

Links

Credits

Written by Romualdo Villalobos

All rights reserved.