Delegates in C++

Introduction

A delegate is a object which can be used to call:

  • a specific method on a specific object
  • a static function

A delegate type is declared like this:

DECLARE_DELEGATE( DelegateType );

This creates the delegate type (not the actual delegate) and that type is then used to create the delegate like so:

DelegateType TheDelegate;

There are many macros like DECLARE_DELEGATE and DECLARE_DELEGATE_OneParam used to create different types of delegates with different parameters. See the references at the end of this page for more details.

Delegate parameters and payload data

There is a difference between the number of parameters the bound method takes and the number of parameters which are passed when invoking the delegate

The delegate declaration macros such as DECLARE_DELEGATE_OneParam, DECLARE_DELEGATE_TwoParams etc. refer to the number and type of parameters passed when invoking the delegate, not the number of parameters the bound method actually takes.

For example we can define a method which takes two parameters and then say we want the delegate to be invoked with only one parameter, like so:

struct FExample
{
	void Method( float F1, float F2 )
	{
	}
};

DECLARE_DELEGATE_OneParam( DelegateType, float );
DelegateType TheDelegate;
TSharedRef Example = MakeShared<FExample>();
TheDelegate.BindSP( Example, &FExample::Method, 2.0f );
TheDelegate.ExecuteIfBound(1.0f);

While the delegate is invoked with one parameter the method will actually be called with two parameters: the one passed to the ExecuteIfBound() function and the one passed to the BindSP() function. The parameters passed to the BindSP() method are refered to as payload parameters:

  • payload parameters are passed to the bound method after the parameters from ExecuteIfBound()
  • payload parameters are specified when the delegate is bound, not when it is invoked, so they are like values captured by a lambda in that they are carried around by the delegate

Binding

When an delegate is created it is unbound, that is to say it does not refer to any method.

Once it has been created it can be bound to a specific function or a method on a specific object. The bind call might:

  • pass the address of a global function (BindStatic)
  • pass a raw c++ pointer to the specific object (BindRaw)
  • pass a lambda (BindLambda)
  • create a weak reference to the specific object (BindSP, BindUObject)

Typically a delegate is created, then bound, and then invoked at a later time, by calling Execute() or ExecuteIfBound():

  • invoking a delegate which references a static function will always work
  • invoking a delegate which references a raw c++ pointer can crash the program if the object that the delegate references has been deleted between the time the delegate was bound and when it was invoked
  • invoking a delegate which uses a weak pointer will not crash if referenced object has been deleted, it just won't do anything

Type conversion

The parameters passed to the bind functions must match the types use by the bound method exactly. For example this code will fail to compile:

struct FExample
{
	void Method( float F1, float F2 )
	{
	}
};

DECLARE_DELEGATE_OneParam( DelegateType, float );
DelegateType TheDelegate;
TSharedRef Example = MakeShared<FExample>();
TheDelegate.BindSP( Example, &FExample::Method, 2.0 );

and results in some hard to decipher error messages:

error C2665: 'TDelegate<void (float),FDefaultDelegateUserPolicy>::BindSP': none of the 2 overloads could convert all the argument types
note: could be 'void TDelegate<void (float),FDefaultDelegateUserPolicy>::BindSP<FExample,ESPMode::ThreadSafe,double>(const TSharedRef<FExample,ESPMode::ThreadSafe> &,void (__cdecl FExample::* )(float,double) const,double)'
note: or       'void TDelegate<void (float),FDefaultDelegateUserPolicy>::BindSP<FExample,ESPMode::ThreadSafe,double>(const TSharedRef<FExample,ESPMode::ThreadSafe> &,void (__cdecl FExample::* )(float,double),double)'
note: 'void TDelegate<void (float),FDefaultDelegateUserPolicy>::BindSP<FExample,ESPMode::ThreadSafe,double>(const TSharedRef<FExample,ESPMode::ThreadSafe> &,void (__cdecl FExample::* )(float,double),double)': cannot convert argument 2 from 'void (__cdecl FExample::* )(float,float)' to 'void (__cdecl FExample::* )(float,double)'
note: Types pointed to are unrelated; conversion requires reinterpret_cast, C-style cast or parenthesized function-style cast
note: see declaration of 'TDelegate<void (float),FDefaultDelegateUserPolicy>::BindSP'
note: while trying to match the argument list '(TSharedRef<FExample,ESPMode::ThreadSafe>, void (__cdecl FExample::* )(float,float), double)'

all because the value 2.0 passed to the BindSP() method is a double whereas the bound method takes a float. The usual implicit type conversion which allows a C++ function which takes a double to be called with a float does not happen when calling a delegate. Changing the double to a float (i.e 2.0 to 2.0f) will fix this.

Dynamic Delegates

Dynamic delegates bind to a specific object by its address but bind the method to be called by the name of that method instead of the address. These are not used much in Lyra but are used in the engine to bind inputs, for example to bind a particular keystroke to a named method on the input component like so:

FInputAxisKeyBinding AB( Binding.AxisKey );
AB.bConsumeInput = Binding.bConsumeInput;
AB.bExecuteWhenPaused = Binding.bExecuteWhenPaused;
AB.AxisDelegate.BindDelegate(ObjectToBindTo, Binding.FunctionNameToBind);

Dynamic delegates are slower than normal delegates because when the delegate is invoked it must interact with the reflection system to find the method by its name. They are also serializable so when they are used in a blueprint they get save with that blueprint.

Multi-cast delegates

A normal delegate can have only one function bound to it at a time. A multi-cast delegate can have more than one function bound to it, so when the delegate is invoked all the functions bound to it are called, one after the other.

The delegate type is declared using some variation of DECLARE_MULTICAST_DELEGATE.

Individual functions are bound one at a time using methods which start with "Add", like this:

DECLARE_MULTICAST_DELEGATE_TwoParams(FCommonSession_FindSessionsFinished, bool bSucceeded, const FText& ErrorMessage);
FCommonSession_FindSessionsFinished OnSearchFinished;
QuickPlayRequest->OnSearchFinished.AddUObject(this, &UCommonSessionSubsystem::HandleQuickPlaySearchFinished, 
    JoiningOrHostingPlayerPtr, HostRequestPtr);
OnSearchFinished.Broadcast(bSucceeded, ErrorMessage);

Similarly to binding a normal delegate, there exist variations to bind different type of functions:

  • AddSP() binds member function and retains a weak pointer to the containing object
  • AddThreadSafeSP() binds member function and retains a weak pointer to the containing object
  • AddUObject() binds a member function and retains a raw pointer to the containing object

Instead of calling Delegate.ExecuteIfBound() you call Delegate.Broadcast() to invoke all the functions bound to the delegate. The bound functions are called in the reverse order in which they were added.

Delegates and Blueprints

Given a delegate type declared like this:

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam( FDelegateType, float, value );

in a class header file we can declare a variable of that type and make it accessible from blueprints like so:

UPROPERTY(BlueprintAssignable, Category="Example Delegates")
FDelegateType OnSomethingInterestingHappening;

Note that the declaration is somewhat different to other definitons:

  • it must be a dynamic multicast delegate
  • the variable names must be listed in the delegate not just the types; in the above C++ code the delegate parameter is called "value" and is of type float.
  • the name of the delegate type must start with 'F'

If we add these lines to LyraCharacter.h

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam( FDelegateType, float, value );
UPROPERTY(BlueprintAssignable, Category="Example Delegates")
FDelegateType OnSomethingInterestingHappening;

then we can edit the blueprint /Game/Characters/Character_Default, click on the "Character_Default" top level component and see the new delegate in the list of possible events:

We can click the '+' button to have Unreal add an event node to the event graph, including the float parameter called "value":

Binding Delegates in Constructors

Don't do this. If you bind delegates in the constructor they become part of the class default object and, at least historically, have cause problems when interacting with blueprints. Bind them in BeginPlay() instead.

Reference

Epic Delegates