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 TDelegate
s 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
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]
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
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
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
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.
class TBaseFunctorDelegateInstance
Holds a functor object.
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.
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.
class TBaseUFunctionDelegateInstance
Holds a weak object pointer and an FName
of a function declared with UFUNCTION
, also stores a cache to the associated UFunction
.
class TBaseUObjectMethodDelegateInstance
Holds a weak object pointer and a pointer to a member function in the UObject
.
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 prefix | Bind 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 prefix | Bind prefix | Concrete IBaseDelegateInstance |
---|---|---|
CreateStatic | BindStatic | TBaseStaticDelegateInstance |
CreateLambda | BindLambda | TBaseFunctorDelegateInstance |
CreateWeakLambda | BindWeakLambda | TWeakBaseFunctorDelegateInstance |
CreateRaw | BindRaw | TBaseRawMethodDelegateInstance |
CreateSP | BindSP | TBaseSPMethodDelegateInstance |
CreateUFunction | BindUFunction | TBaseUFunctionDelegateInstance |
CreateUObject | BindUObject | TBaseUObjectMethodDelegateInstance |
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