Raw Source Code

Unreal delegates under the hood

Understand how the unreal delegates works under the hood by implementing your own minimalist TDelegate class clone which can hold references to free and member functions uniformly.

Delegate systems are central part in software development, they allow decoupling between distinct components on nicely architected programs and show up naturally when talking about events, event emitters, the observer pattern and more.

Today we aim to understand how Unreal implements its delegate system under the hood by building our own delegate clone class which will hold/bind references to member functions of raw C++ objects and free functions on a uniform and type-safe way easy to extend.

Unreal can also hold references to member functions of UObject objects, Lambda/Functors and Shared Pointers.

We intentionally exclude Unreal sophisticated memory handling and other details like the bound payload in order to focus our attention to class hierarchies, template specializations and general structure.


Part 1: IBaseDelegateInstance

Abstract class representing a "pointer holder", distinct type of pointers (free, member functions) will extend from this class.

Note that IBaseDelegateInstance is a forward declared templated class.

template <typename FuncType>
struct IBaseDelegateInstance;

Then we find the following definition which is also:

  • A class template with additional RetType, and variadic ArgTypes parameters
  • IBaseDelegateInstance specialized for function types.
template <typename RetType,
  typename... ArgTypes>
struct IBaseDelegateInstance
  <RetType(ArgTypes...)>
{
 /**
  * Execute the delegate.
  If the function pointer
  is not valid, an error
  will occur.
  */
  virtual RetType
  Execute(ArgTypes...) const = 0;
};

Part 2: TMemFunPtrType

Forward declared template class that will be used to generate the signature of a "pointer to member function" type.

template <bool Const,
  typename Class,
  typename FuncType>
struct TMemFunPtrType;

Similarly as before we find the following definition which is:

  • A class template with additional Class, RetType and variadic ArgTypes parameters
  • TMemFunPtrType specialized for when Const is false and when FuncType is RetType(ArgTypes...).
template <typename Class,
  typename RetType,
  typename... ArgTypes>
struct TMemFunPtrType<false,
  Class,
  RetType(ArgTypes...)>
{
  typedef RetType(Class::* Type)(ArgTypes...);
};

And for pointers to constant member functions we have the following:

  • TMemFunPtrType specialized for when Const is true and when FuncType is RetType(ArgTypes...).
template <typename Class,
  typename RetType,
  typename... ArgTypes>
struct TMemFunPtrType<true,
  Class,
  RetType(ArgTypes...)>
{
  typedef RetType(Class::* Type)(ArgTypes...) const;
};

Part 2: TBaseRawMethodDelegateInstance

Implements a delegate binding for C++ member functions, this class is intended to hold a pointer to an object and a pointer to a function in the object. First we find the following forward declared template class

template <bool bConst,
  class UserClass,
  typename FuncType>
class TBaseRawMethodDelegateInstance;

And the following definition which finally holds pointer to things:

  • A class template with additional parameters:
    • bConst: Is TBaseRawMethodDelegateInstance holding a constant member function?
    • UserClass: The type of the object that has the member function.
    • WrappedRetValType: The type of the return value of the member function.
    • Variadic ParamTypes: The type of the parameters of the member function (if any).
template <
  bool bConst,
  class UserClass,
  typename WrappedRetValType,
  typename... ParamTypes>
  • TBaseRawMethodDelegateInstance specialized for:
    • when Const is true or false
    • when UserClass is whatever it comes from template argument UserClass
    • when FuncType is WrappedRetValType(ArgTypes...).
class TBaseRawMethodDelegateInstance<
  bConst,
  UserClass,
  WrappedRetValType(ParamTypes...)>
  • A class that extends (and implements) the abstract class IBaseDelegateInstance with FuncType evaluated to the function type WrappedRetValType(ParamTypes...)
  : public IBaseDelegateInstance<
  WrappedRetValType(ParamTypes...)>
{
  • Typedef of pointer to member function
public:
  using FMethodPtr = typename TMemFunPtrType<
    bConst,
    UserClass,
    WrappedRetValType(ParamTypes...)>::Type;
  • Constructor receiving:
    • Pointer to an UserClass object.
    • Pointer to a member function in the object.
  TBaseRawMethodDelegateInstance(
    UserClass* InUserObject,
    FMethodPtr InMethodPtr)
  : UserObject(InUserObject), MethodPtr(InMethodPtr)
  {
    // ORIGINAL UNREAL NOTE:
    // Non-expirable delegates must always
    // have a non-null object pointer on
    // creation (otherwise they could never execute.)
    // check(InUserObject != nullptr &&
    //       MethodPtr != nullptr);
  }
  • Implements the Execute method from the abstract class IBaseDelegateInstance<WrappedRetValType(ParamTypes...)> here is where we finally invoke the function pointer.
  WrappedRetValType Execute(
    ParamTypes... Params) const override
  {
    // NOTE: This is an oversimplified version
    // check unreal source code for more fun.
    return (UserObject->*MethodPtr)(Params...);
    //return std::invoke(MethodPtr, ParamTypes...);
  }
  • Class defines the following fields to hold the pointer to UserClass object and its member function.
private:
  UserClass* UserObject;
  FMethodPtr MethodPtr;
};

Exercise 1: Free function pointers

Based on previous notes implement your own IBaseDelegateInstance class which will be used to hold pointers to free functions (Solution at end of this article).

Part 3: TMyDelegate

The macro DECLARE_DELEGATE(DelegateName) declares a TDelegate type, so you can create your own objects of this type. Also note that in this document I renamed this to TMyDelegate in order to hint that the code shown below is an extremely over simplified version inspired from the original Unreal code and is only intended for study to interested readers on more in-depth diving into Unreal internals.

First we find the following templated class that will be used if you tell the compiler to instantiate something that is not a function e.g.: TMyDelegate<int>, read the next section to understand why this works.

/**
 * Unicast delegate template class.
 *
 * Use the various DECLARE_DELEGATE
 * macros to create the actual delegate
 * type, templated to the function
 * signature the delegate is compatible with.
 * Then, you can create an instance
 * of that class when you want to
 * bind a function to the delegate.
 */
template<typename DelegateSignature>
class TMyDelegate
{
    static_assert(sizeof(DelegateSignature) == 0, "Expected a function signature for the delegate template parameter");
};

Now you find TMyDelagate specialized for when DelegateSignature is the function type InRetValType(ParamTypes...), otherwise it will fallback to the non-specialized version defined previously.

template<typename InRetValType, typename... ParamTypes>
class TMyDelegate<InRetValType(ParamTypes...)>
{

}

Unreal defines the following type aliases.

/* ...class TMyDelegate declaration... */
{
  using FuncType = InRetValType(ParamTypes...);
  using IBaseDelegateInstanceType = IBaseDelegateInstance<FuncType>;
  using RetValType = InRetValType;
}

And the static Create functions which you usually use to create a delegate object and bind it to a function:

  /**
  * Static: Creates a raw C++ pointer
  * member function delegate.
  *
  * Raw pointer doesn't use any sort of reference,
  * so may be unsafe to call if the object was
  * deleted out from underneath your delegate.
  * Be caref  ul when calling Execute()!
  */
  template <typename UserClass>
  inline static TMyDelegate<RetValType(ParamTypes...)>
  CreateRaw(  
    UserClass* InUserObject,
    typename TMemFunPtrType<false,
      UserClass,
      RetValType(ParamTypes...)
    >::Type InFunc)
  {
    TMyDelegate<RetValType(ParamTypes...)> Result;

    // Instance the Delegate instance
    Result.LocalDelegateInstance.reset(
    new TBaseRawMethodDelegateInstance<false,
      UserClass,
      FuncType>(InUserObject, InFunc));

    return Result;
  }

We also create our own version of the Execute method:

  inline RetValType Execute(ParamTypes... Params)
  {
    return LocalDelegateInstance->Execute(Params...);
  }

And as a final simplification we create a field to hold our bound IBaseDelegateInstanceType object.

private:
  std::shared_ptr<IBaseDelegateInstanceType>
    LocalDelegateInstance;

Solution to exercise 1:

Below I put my own IBaseDelegateInstanceType code for free function delegate instances, which you won't find on real Unreal code because I completely came up with this as exercise, instead you should look for TBaseStaticDelegateInstance.

/**
 * Implements a delegate binding for C++ free functions.
 */
template <typename FuncType>
class TBaseFreeFunctionDelegateInstance;

template <typename WrappedRetValType,
  typename... ParamTypes>
class TBaseFreeFunctionDelegateInstance<
  WrappedRetValType(ParamTypes...)>
  : public IBaseDelegateInstance<
    WrappedRetValType(ParamTypes...)>
{
public:
  using FFreeFunctionPtr = WrappedRetValType(*)(ParamTypes...);

  TBaseFreeFunctionDelegateInstance(FFreeFunctionPtr InFuncPtr)
  : FuncPtr(InFuncPtr)
  {

  }

  WrappedRetValType Execute(ParamTypes... Params) const override
  {
    return (*FuncPtr)(Params...);
  }

private:
  FFreeFunctionPtr FuncPtr;
};

Part 4: Using "my delegate" class

We start defining the following class with a dummy member function.

/* Dummy class with a method that
will be bound to a delegate*/
class MyClass
{
public:
    void HandleSomething(int Num, float Num2, const std::string& Name)
    {
        std::cout << "Handling something on member function listener with " << Num << " and " << Num2 << " by " << Name << std::endl;
    }
};

And also the following free function just for testing.

/* Dummy free function that will be bound to a delegate*/
void HandleSomethingFromFreeFunction(int Num, float Num2, const std::string& Name)
{
    std::cout << "Handling something on free function listener with " << Num << " and " << Num2 << " by " << Name << std::endl;
}

Finally we instantiate our delegate objects and bound them to some functions.


int main()
{
    MyClass obj;

    using MyDelegateSignature = TMyDelegate<void(int, float, const std::string&)>;

    // An invocation list whose targets are member function and free functions
    MyDelegateSignature MultiCastInvocationList[2]{
        MyDelegateSignature::CreateRaw(&obj, &MyClass::HandleSomething),
        MyDelegateSignature::CreateFreeFunction(&HandleSomethingFromFreeFunction)
    };

    // Invoke all the delegates (somehow similar to unreal multi-cast delegates)
    for (MyDelegateSignature& Delegate : MultiCastInvocationList)
    {
        Delegate.Execute(5, 10.0f, "Romualdo Villalobos");
    }
}

Final comments:

Delegates on Unreal is a very robust system with a lot more of complexity than shown in this article. Here we only explored the surface of some of the classes involved on unicast delegates and on the final section we also noted how could we possibly create our own multicast delegates.

As a final comment please note this document consists of study notes on the delegate system internals and as such is very distanct from the full picture. More fun is granted for you on the real source code.

Links

Credits

Written by Romualdo Villalobos

All rights reserved.