UE4技术总结——委托

UE4技术总结——委托

在开始前先说明,这部分内容主要基于UE 4.26.2版本[1]编写,因此读者可能需要注意时效性。如果您对源代码感兴趣,可以直接阅读引擎的DelegateCombinations.hDelegate.h以及相关代码。

因为是一个非常基础,时不时会用到的功能,所以这里就不介绍使用场景了,直接进入正题。

一、定义

首先,官方定义如下[2]

委托 是一种泛型但类型安全的方式,可在C++对象上调用成员函数。可使用委托动态绑定到任意对象的成员函数,之后在该对象上调用函数,即使调用程序不知对象类型也可进行操作。复制委托对象很安全。你也可以利用值传递委托,但这样操作需要在堆上分配内存,因此通常并不推荐。请尽量通过引用传递委托。

同时,根据官方文档,虚幻引擎支持3种类型的委托:

  1. 单播委托
  2. 多播委托
    1. 事件
  3. 动态委托

之所以说是3种,是因为事件实际上在现在的版本中差不多就是多播委托(当然,实际上还是有些许不同的,主要是函数调用权限和多播不同,但是实际上也没有措施保证函数被不是拥有者的对象调用,因此读者只需要理解为多播委托即可)[3]。而且在UE的4.26.2版本源码中已经标明,事件类型的委托将会在后面更新的版本移除掉:

image-20210903102550677

因此,我们主要重点还是放在单播、多播、动态委托上,事件不会进行详细说明。

同时,UE4中存在由基本委托组合起来的委托,但是在介绍组合的委托之前我们先看看这3种基本委托。

接下来我们先简单看看该怎么用。

顺带一提,这里我默认读者知道如何在C++中实现委托,如果您还不清楚,那么建议阅读文末参考中列出的的文章[4](了解即可)。

二、用法

2.1 声明与调用委托

UE4中的委托都通过宏定义来声明,随后就可以通过宏定义声明的委托来声明对应的委托变量,实际使用的时候会通过将函数绑定到委托变量来使用。

2.1.1 单播委托

  1. 单播委托只能绑定一个函数指针,执行委托的时候也只能触发一个函数;

  2. 单播委托绑定的函数可以有返回值,这点和多播委托不同;

2.1.1.a 声明
// 无返回值函数的委托// 无参数DECLARE_DELEGATE(DelegateName);// 1个参数DECLARE_DELEGATE_OneParam(DelegateName, Param1Type);// <num>个参数,最多到9个DECLARE_DELEGATE_<num>Params(DelegateName, Param1Type, Param2Type, ...);// 有返回值// 无参数DECLARE_DELEGATE_RetVal(RetValType, DelegateName);// 1个参数DECLARE_DELEGATE_RetVal_OneParam(RetValType, DelegateName, Param1Type);// 多个参数,最多到9个DECLARE_DELEGATE_RetVal_<num>Params(RetValType, DelegateName, Param1Type, Param2Type, ...);

一个简单的声明单播委托的例子:

// 直接用宏定义在顶部声明就可以了DECLARE_DELEGATE(FLearningDelegate);class XXX_API ALearnDelegateActor : public AActor{    GENERATED_BODY()public:    // ... 省略public:    // 单播委托带有UPROPERTY宏,不能添加BlueprintAssignable标识符,动态多播才可以声明BlueprintAssignable    FTestDelegate FLearningDelegate;}
2.1.1.b 绑定

在绑定函数之前我们先要声明委托和委托变量:

// 单播无参数的委托,其他类型的单播委托如此类推// 这行通常放在头文件的上方,类定义之外,毕竟是宏DECLARE_DELEGATE(FSingleDelagateWithNoParam);// 用上面声明的委托声明委托变量// 这里放在类定义中,作为一个属性进行定义FSingleDelagateWithNoParam SingleDelagateWithNoParam;

然后我们就可以绑定函数了,绑定函数的API有很多种,但是最常用的还是BindUObject,因此这里以BindUObject举例:

// ADelegateListener::EnableLight的定义类似于void ADelegateListener::EnableLight(),没有参数,也没有返回值// 这个绑定假设是在类里面绑定的,所以用了this,实际上可以是别的UObjectSingleDelagateWithNoParam.BindUObject(this, &ADelegateListener::EnableLight)

下面这张图列举了除了BindUObject之外还能够使用什么函数进行绑定,以及在什么情况下使用[2:1]

除了BindUObject之外还有别的绑定函数,这里直接借用官网过时的文档中的列表:

image-20211005172900827

大概如上,都非常简单,在使用的时候按照您要绑定的函数来选择对应的函数来绑定即可。这里简单补充几个官网文档没有提及的绑定:

函数 描述
BindThreadSafeSP(SharedPtr, &FClass::Function) 用一个弱指针TWeakPtr来绑定一个原生C++类成员函数,当指针SharedPtr指向的对象无效的时候不会执行绑定的回调函数
BindWeakLambda(UObject, Lambda) 绑定一个匿名函数,在传入的UObject有效,还没有被回收的时候都可以调用这个匿名函数。这个匿名函数中可以用this,但是其他关键词不一定能用
BindUFunction(UObject, FName("FunctionName")) 用来绑定一个UObject的UFUNCTION函数,原生的与动态的委托都可以用这个函数来绑定回调函数

这里提几个注意事项[5]

  1. 注意BindRaw绑定的普通C++对象的成员函数,要特别注意执行的时候这个对象有没有被销毁。如果被销毁了那么触发委托执行绑定的函数会导致报错;
  2. 注意BindLambda绑定的Lambda表达式捕获的外部变量,如果在触发委托的时候会导致报错;
  3. BindWeakLambdaBindUObjectBindUFunction绑定时会弱引用一个UObject对象,需要预先IsBound()或者ExecuteIfBound来判断是否该对象还有效再执行委托,否则可能会报错;
  4. BindSPBindThreadSafeSP绑定时会弱引用一个智能指针对象(UE4的智能指针),执行前需要先IsBound()或者ExecuteIfBound来判断该对象是不是还存在,否则可能会报错;
  5. 如果单播委托对象被销毁,那么析构函数会自动调用UnBind进行解绑;
2.1.1.c 执行委托

执行单播委托需要调用的函数主要是Execute(您要传入的参数),要注意的是,这个函数并不会检查您的绑定情况,因此如果委托未绑定,那么直接执行此函数会导致报错。因此往往推荐在调用Execute(传入参数)前先用IsBound()来检查是否已经进行了绑定。当然也可以直接调用ExecuteIfBound(传入参数),这个函数等效于if(委托.IsBound())进行判断后再执行Execute(传入参数)

image-20211005223951296

2.1.1.d PayLoad

首先介绍下PayLoad的功能,PayLoad是委托绑定的时候传入的额外参数列表,保存在委托对象内。触发委托的时候PayLoad会跟着Execute(传入的参数)ExecuteInBound(传入的参数)传入的参数之后填充到绑定函数的参数列表中,然后执行。

举个例子:

DECLARE_DELEGATE_OneParam(FLearnDelegate, float);static void LearningDelegate(float Bar) {    UE_LOG(LogTemp, Log, TEXT("=== INFO: FOOO %f ==="), Bar);}static void LearningPayload(float Bar, FString Test) {    UE_LOG(LogTemp, Log, TEXT("=== INFO: FOOO %f, %s ==="), Bar, *Test);}// 在GameInstance的初始化函数中或者其他地方// 正常使用FLearnDelegate DelegateObj1;DelegateObj1.BindStatic(LearningDelegate);DelegateObj1.ExecuteIfBound(23.0f);// PayLoadFLearnDelegate DelegateObj2;// 这里的“TEST”会在调用绑定函数的时候紧接着委托对戏那个传入的参数传入DelegateObj2.BindStatic(LearningDelegate, FString(TEXT("TEST!")));// “TEST”会接在23.0f后面,所以最后是传入到Test参数中DelegateObj2.ExecuteIfBound(23.0f);
2.1.1.e 底层实现
绑定函数指针

相关代码在DelegateCombination.h以及Delegate.h中。

首先我们需要有个大体的概念,其实本质上就是保存了一个函数指针,在执行的时候直接访问该函数指针对应的函数即可,如果是成员函数则比较特殊,需要同时知道成员函数所在的类,同时应该有一个指针指向该对象。接下来我们看具体实现。

image-20211005233553067

image-20211005233615088

可以看到,实际上就是通过TDelegate这个类来实现的,所以实际上我们在定义委托的时候就是在调用TDelegate<returntype(一堆您传入的参数)>创建委托类型,并通过typedef重命名为您给定的名字,方便记忆与阅读。TDelegateDelegateSignatureImpl.ini中实现。因为内容比较多,因此我们只看关键部分。首先我们看到他继承了TDelegateBase这个类:

image-20211006153548452

简单扫几眼,就会发现实际上用来保存指向函数的指针并不在TDelegate中,而应该是放在了父类,也就是TDelegateBase

image-20211006164312068

image-20211006155731780

读者可能发现了UserPolicy这个参数,这里实际上是4.26版本才新加入的内容[6]

之前的静态单播的基类是FDelegateBase,这个类没有变化,但是所有的public接口被改成了protected,无法直接使用了。这一点真是非常糟糕,哪有增加可扩展性的同时把接口都藏起来的,本来所有实现就都是写到头文件里的。

最大的不同是接下来的地方,其实现不是通过直接对FDelegateBase的继承完成的,而是通过一个叫做FDefaultDelegateUserPolicy的结构体进行中转的。这个结构体中只定义了三个类型的别名,分别是FDelegateInstanceExtrasFDelegateExtrasFMulticastDelegateExtras。其中FDelegateExtras指向的就是FDelegateBase

静态单播的实现类TDelegateBase(原来叫TBaseDelegate,这诡异的命名)变成了模板类,该类继承于模板参数中的FDelegateExtras类型。说到这里我想应该已经明白了UE4这个改动的含义。这意味着我们可以通过自己定义一个FDefaultDelegateUserPolicy以外的其他结构体UserPolicy,并在其中定义上述三个类型,就可以釜底抽薪式地把写在底层的实现替换成我们自定义的实现,这无疑很大地增加了这个模块的可扩展性。

简单的说就是FDelegateBase在经过抽象之后,允许用户单独创建一个UserPolicy结构体给TDelegateBase来自定义委托,当然如果没有传入自己定义的UserPolicy的话,那么会使用默认的FDefaultDelegateUserPolicy(这里用到了C++的模板偏特化特性[7][8],能够在给定默认值的同时,能够让用户输入自己希望的值):

image-20211006194851272

因此实际上此处的UserPolicyFDefaultDelegateUserPolicy,那么我们简单看看FDefaultDelegateUserPolicy这一struct的内容:

struct FDefaultDelegateUserPolicy{    // 这里的using是别名指定    using FDelegateInstanceExtras  = IDelegateInstance;    // 注意下面这个,另外两个会在其他委托中用到,先不管    using FDelegateExtras          = FDelegateBase;    using FMulticastDelegateExtras = TMulticastDelegateBase<fdefaultdelegateuserpolicy>;};

回到开始的TDelegate<inretvaltype(paramtypes...), userpolicy=""> : public TDelegateBase<userpolicy>,我们看看TDelegateBase的定义:

image-20211006195733065

所以实际上最终还是继承了FDefaultDelegateUserPolicy::FDelegateExtras,即FDelegateBase

我们继续追踪GetDelegateInstanceProtected(),继续看TDelegateBase,但是我们会发现,实际上TDelegateBase也没有保存指针,只是提供了一系列函数(如,是否已经绑定了函数的IsBound()等):

template <typename userpolicy="">class TDelegateBase : public UserPolicy::FDelegateExtras{    template <typename>    friend class TMulticastDelegateBase;    // 用using指定别名    using Super = typename UserPolicy::FDelegateExtras;public:    // 省略部分注释与宏判断    FName TryGetBoundFunctionName() const    {        // 注意这里,可以看出不是这里保存的函数指针        if (IDelegateInstance* Ptr = Super::GetDelegateInstanceProtected())        {        // 实际上还是调用了委托对象提供的函数来实现具体的功能        return Ptr->TryGetBoundFunctionName();        }    return NAME_None;    }    // 省略一系列函数}

可以看到,实际上即便是TDelegateBase,也是要通过Super::GetDelegateInstanceProtected()来获取委托对象,这个函数最终调用FDelegateBase类提供的GetDelegateInstanceProtected()来获取委托对象(注意using Super = typename UserPolicy::FDelegateExtras;,而在FDefaultDelegateUserPolicy中,using FDelegateExtras = FDelegateBase;),最终通过IDelegateInstance类的委托对象提供的函数来实现相关功能。因此我们还得要接着往下面看才能找到真正保存函数指针的地方。

因此,我们看到FDelegateHandle

class FDelegateBase{    template <typename>    friend class TMulticastDelegateBase;    template <typename>    friend class TDelegateBase;  protected:    /**     * Creates and initializes a new instance.     *     * @param InDelegateInstance The delegate instance to assign.    */    explicit FDelegateBase()        : DelegateSize(0)    {    }    ~FDelegateBase()    {    // 可以看到实际上在被销毁的时候会自动调用函数取消绑定        Unbind();    }    // 省略部分函数      // 这里是重点    /**     * Gets the delegate instance.  Not intended for use by user code.     *     * @return The delegate instance.     * @see SetDelegateInstance     */    FORCEINLINE IDelegateInstance* GetDelegateInstanceProtected() const    {        return DelegateSize ? (IDelegateInstance*)DelegateAllocator.GetAllocation() : nullptr;    }    // 省略函数private:    // 这个也是重点    FDelegateAllocatorType::ForElementType<falignedinlinedelegatetype> DelegateAllocator;    int32 DelegateSize;}

上面提到,TDelegateBase最终调用的是FDelegateBase提供的GetDelegateInstanceProtected(),而这里我们可以看到,实际上是返回IDelegateInstance类型的数据(这里先忽略掉DelegateAllocator,只需要理解为一个工具类,用来分配内存,因为与委托不太相关所以先不详细说明,如果感兴趣可以看这篇文章[9]),因此最终函数指针理论上是包裹在IDelegateInstance中的。

但是再想想,实际情况肯定没有这么简单,还记得我们前面说到的绑定函数吗?实际可能传入的函数指针类型非常多,例如可能传入一个在UObject对象中的成员函数,可能传入一个lambda函数等。所以实际上,会包裹在IDelegateInstance为基类的,根据各种传入函数指针类型进行适配的派生类中。

例如,接着上面往下看,我们可以看到这类型的函数:

/**     * Static: 用来创建C++全局函数指针的委托     */    template <typename... vartypes="">    UE_NODISCARD inline static TDelegate<retvaltype(paramtypes...), userpolicy=""> CreateStatic(typename TIdentity<retvaltype (*)(paramtypes...,="" vartypes...)="">::Type InFunc, VarTypes... Vars)    {        TDelegate<retvaltype(paramtypes...), userpolicy=""> Result;    // 重点是下面这个,TBaseStaticDelegateInstance的基类就是IDelegateInstance        TBaseStaticDelegateInstance<functype, userpolicy,="" vartypes...="">::Create(Result, InFunc, Vars...);        return Result;    }    /**     * Static: 创建lambda函数的委托     */    template<typename functortype,="" typename...="" vartypes="">    UE_NODISCARD inline static TDelegate<retvaltype(paramtypes...), userpolicy=""> CreateLambda(FunctorType&& InFunctor, VarTypes... Vars)    {        TDelegate<retvaltype(paramtypes...), userpolicy=""> Result;        TBaseFunctorDelegateInstance<functype, userpolicy,="" typename="" tremovereference<functortype="">::Type, VarTypes...>::Create(Result, Forward<functortype>(InFunctor), Vars...);        return Result;    }// 还有更多,这里忽略

简单看下TBaseStaticDelegateInstance

image-20211007155244395

可以很轻松找到保存C++函数指针的变量(这个变量类型是UE4提供的专门用来保存C++函数指针的类型,网上资料很多[10],这里就不进行介绍了)。

同理,相似的,绑定UObject对象成员函数委托创建函数则有:

image-20211007160916551

最终执行的时候的形式就类似于这样:

// 全剧函数(*MyDelegate)();// 对象成员函数(MyObj->*FuncPtr)();// 如果是在栈上(StackObj.*Func1Ptr)();
Payload的实现

当然实际上UE4中会支持Payload,会先把一部分预先输入的参数拼接到调用委托的时候传入的参数后面去,形成一个参数列表,最后一起作为参数输入到绑定函数,但是原理差不多。

以全局函数的执行为例:

image-20211007172150523

Payload实际上是一个TTuple

image-20211007172225486

最终执行:

image-20211007172259116

因为Payload特性前面介绍过,所以这里不赘述。

绑定

但是只有创建是不行的,这时候的委托还没有绑定上要执行的函数。我们还是以绑定全局函数为例:

/**     * 绑定一个C++全局函数     */    template <typename... vartypes="">    inline void BindStatic(typename TBaseStaticDelegateInstance<functype, userpolicy,="" vartypes...="">::FFuncPtr InFunc, VarTypes... Vars)    {        *this = CreateStatic(InFunc, Vars...);    }

结合上面的CreateStatic的实现就可以明白了,因为CreateStatic返回的是右值,这里左侧的*this=会调用到TDelegate的Move Assigment Operator:

    /**     * Move assignment operator.     *     * @param    OtherDelegate    Delegate object to copy from     */    inline TDelegate& operator=(TDelegate&& Other)    {        if (&Other != this)        {            // this down-cast is OK! allows for managing invocation list in the base class without requiring virtual functions            DelegateInstanceInterfaceType* OtherInstance = Other.GetDelegateInstanceProtected();            if (OtherInstance != nullptr)            {                OtherInstance->CreateCopy(*this);            }            else            {                Unbind();            }        }        return *this;    }

最终将创建出来的TDelegate赋值给自身,从而实现绑定函数。

绑定不同的函数指针对应不同的T<函数指针类型>DelegateInstance<...>::Create(...),这里列举下,实际上看源代码也可以理解:

创建函数 对应的Delegate Instance创建函数
CreateStatic() TBaseStaticDelegateInstance<...>::Create(...)
CreateLambda() TBaseFunctorDelegateInstance<...>::Create(...)
CreateWeakLambda() TWeakBaseFunctorDelegateInstance<...>::Create(...)
CreateRaw() TBaseRawMethodDelegateInstance<...>::Create(...)
CreateSP() TBaseSPMethodDelegateInstance<...>::Create(...)
CreateThreadSafeSP() TBaseSPMethodDelegateInstance<...>::Create(...)
CreateUFunction() TBaseUFunctionDelegateInstance<...>::Create(...)
CreateUObject() TBaseUObjectMethodDelegateInstance<...>::Create(...)
补充

这里看起来没有介绍带参数、返回值的情况,因为实际上带参数、返回值的也是调用了FUNC_DECLARE_DELEGATE,调用的typedef都一样,只是传入到模板的参数数量不一样(借助了C++11中的可变模板参数实现)。

image-20211006021802515

最终还是:

image-20211006021832263

image-20211007212745935

另外,这里的__VA_ARGS__实际上就是:

image-20211006021854475

比较容易理解,所以这里不作详细解释。

2.1.1.f 总结

总而言之,单播委托的使用流程如下图所示:

graph TD; 开始 –> 使用宏定义委托类型 –>声明委托对象–> 绑定需要执行的函数指针到委托对象上–>|需要的时候|触发委托对象并执行指针指向的函数; 触发委托对象并执行指针指向的函数–>|不再需要绑定的函数|从委托对象中解绑函数–>|不再需要委托对象|销毁委托对象; 从委托对象中解绑函数–>|绑定新的函数|绑定需要执行的函数指针到委托对象上; 触发委托对象并执行指针指向的函数–>|指向的函数失效|报错;

而委托的类层次结构我们可以总结为(其实我不是特别熟悉UML图,希望没有错):

classDiagram FDelegateBase <|– TDelegateBase TDelegateBase <|– TDelegate TDelegate ..|> FDefaultDelegateUserPolicy TDelegateBase ..|> FDefaultDelegateUserPolicy FDelegateBase *– IDelegateInstance IDelegateInstance <|– IBaseDelegateInstance IBaseDelegateInstance <|– TCommonDelegateInstanceState TCommonDelegateInstanceState <|– TBaseStaticDelegateInstance TCommonDelegateInstanceState <|– TBaseFunctorDelegateInstance TCommonDelegateInstanceState <|– TWeakBaseFunctorDelegateInstance TCommonDelegateInstanceState <|– TBaseRawMethodDelegateInstance TCommonDelegateInstanceState <|– TBaseSPMethodDelegateInstance TCommonDelegateInstanceState <|– TBaseUFunctionDelegateInstance TCommonDelegateInstanceState <|– TBaseUObjectMethodDelegateInstance TDelegate *– TBaseStaticDelegateInstance TDelegate *– TBaseFunctorDelegateInstance TDelegate *– TWeakBaseFunctorDelegateInstance TDelegate *– TBaseRawMethodDelegateInstance TDelegate *– TBaseSPMethodDelegateInstance TDelegate *– TBaseUFunctionDelegateInstance TDelegate *– TBaseUObjectMethodDelegateInstance FDefaultDelegateUserPolicy : +FDelegateInstanceExtras(IDelegateInstance) FDefaultDelegateUserPolicy : +FDelegateExtras(FDelegateBase) FDefaultDelegateUserPolicy : +FMulticastDelegateExtras(TMulticastDelegateBase<fdefaultdelegateuserpolicy>) class TDelegate { +CreateStatic(…) +CreateLambda(…) +CreateWeakLambda(…) +CreateRaw(…) +CreateSP(…) +CreateThreadSafeSP(…) +CreateUFunction(…) +CreateUObject(…) +TDelegate(TDelegate&& Other) +TDelegate& operator=(TDelegate&& Other) +BindStatic(…) +BindLambda(…) +BindWeakLambda(…) +BindRaw(…) +BindSP(…) +BindThreadSafeSP(…) +BindUFunction(…) +BindUObject(…) } class TDelegateBase { -class TMulticastDelegateBase -Super(FDelegateBase) } class FDelegateBase { #FDelegateBase& operator=(FDelegateBase&& Other) #void Unbind() #IDelegateInstance* GetDelegateInstanceProtected() const -void* Allocate(int32 Size) -FDelegateAllocatorType::ForElementType<falignedinlinedelegatetype> DelegateAllocator -int32 DelegateSize } class IDelegateInstance { <<interface>> +FName TryGetBoundFunctionName() +UObject* GetUObject() +const void* GetObjectForTimerManager() const +uint64 GetBoundProgramCounterForTimerManager() const +bool HasSameObject( const void* InUserObject ) const +bool IsCompactable( ) +bool IsSafeToExecute( ) +FDelegateHandle GetHandle() const } class TCommonDelegateInstanceState { #TTuple<vartypes…> Payload #FDelegateHandle Handle +RetValType(InRetValType) }

此外:

  1. 单播委托支持PayLoad功能,但是动态类型的委托并不支持PayLoad;
  2. 单播委托在执行之前必须要IsBound()检查是否已经绑定,否则会报错;
  3. 单播委托允许绑定函数带有返回值;

其实单播委托理解了后面的都不难理解了,因此后面的内容会没有单播委托这么详细(毕竟实现都相似的)。


2.1.2 动态(单播)委托

注意:这里讨论的是动态单播委托,动态多播委托后面会另外介绍

  1. 动态其实是指能够被序列化,允许动态绑定,除此之外实际上和单播代理没有太大区别;

  2. 动态委托也可以有返回值,但是只能有一个返回值;

  3. 动态即能够被序列化,因此可以在蓝图中使用,也可以添加UPROPERTY

  4. 绑定的时候不需要函数指针,只需要函数名称字符串,但是只能够绑定UFUNCTION

  5. 动态委托运行是最慢的,所以如果没有必要的话就用别的委托;

2.1.2.a 声明

其实和单播委托的声明差不多:

DECLARE_DYNAMIC_DELEGATE[_RetVal, ...]( DelegateName );// 例如无参数,无返回值DECLARE_DYNAMIC_DELEGATE(FNoRetNoParamDelegate);// 例如1个参数无返回值,最多支持到9个,注意和前面不同,给定参数类型的同时要加上类型名字,并且绑定参数和委托要保持一致DECLARE_DYNAMIC_DELEGATE_OneParam(FOnAssetLoaded, class UObject*, Loaded);// 例如1个返回值一个参数DECLARE_DYNAMIC_DELEGATE_RetVal_OneParam(UWidget*, FGenerateWidgetForObject, UObject*, Item); 
2.1.2.b 绑定

4.26的动态委托绑定函数只有一个BindUFunction,并提供UObject对象、函数名字即可。

2.1.2.c 执行委托

和单播委托类似:

image-20211007234315173

2.1.2.d 底层实现

image-20211007234357644

可以看到实际上依托TBaseDynamicDelegate来实现,而且宏定义声明一个动态委托就是声明了一个类继承TBaseDynamicDelegate。宏定义里面也另外定义了ExecuteIfBoundExecute函数,实际执行委托也是通过宏定义里面定义的这两个函数,同时依托UE4的反射、序列化机制实现的。

TBaseDynamicDelegate内的实现不多,实际上还是得依靠TScriptDelegate

image-20211007234637981

TScriptDelegate才是真正保存函数名字、绑定的对象的弱指针的地方:

image-20211007234804808

简单看下绑定部分,因为只能绑定UFUNCTION函数,所以只有一个绑定函数:

image-20211007234834434

执行则是依托一开始宏定义里面定义的Execute(传入参数)

image-20211007234952902

实际执行的时候UE4会根据输入的函数名字找到对应的函数并执行,这个函数最终会被上面定义的Execute调用:

image-20211008000445308

2.1.2.e 总结

因为比较简单,所以这里就先不花UML图来解析了。

  1. 实际上声明一个动态委托类型就是创建了一个继承TBaseDynamicDelegate的类,并且类名为动态委托的名字;
  2. 动态委托在执行时需要实时在类中按照给定的函数名字查找对应的函数,因此执行速度很慢,所以如果能用别的不是动态的委托代替就用别的委托[11]
  3. 动态委托能够被蓝图调用;
  4. 动态委托能够序列化,因此可以被存储到硬盘上;
  5. 绑定的时候不需要函数指针,只需要函数名称字符串,但是只能够绑定UFUNCTION

2.1.3 多播委托

吐个槽,官方文档真的是一言难尽,只是multicast delegate这个词在中文页面上都有2种不同的翻译。更加关键的是,多播委托的官方文档居然还有低级错误,在《多播委托》页面最上面写明了“多播委托不能使用返回值”,下面给的声明多播委托示例就带了个返回值。

  1. 多播委托能绑定多个函数指针,委托被执行的时候也会触发多个函数;
  2. 多播委托执行的时候,执行绑定该委托的函数的顺序实际上是没有规定的(因此可能最后绑定的函数最先被执行)
  3. 多播委托不允许有返回值。实际上底层是一个保存了所有绑定了这个委托的函数的FDelegateBase数组,执行委托的时候会遍历数组并调用绑定的函数
2.1.3.a 声明
DECLARE_MULTICAST_DELEGATE<参数数量>( DelegateName, ParamsTypes );// 例如0个参数DECLARE_MULTICAST_DELEGATE( DelegateName );// 例如1个参数DECLARE_MULTICAST_DELEGATE_OneParam( DelegateName, Param1Type );

比较简单,和前面的委托都差不多。

2.1.3.b 绑定

先以绑定UObject对象的成员函数为例:

UDelegatepTestClass* UObjMC = NewObject<udelegateptestclass>(this, UDelegatepTestClass::StaticClass());// 先传入UObject,然后传入成员函数指针CharacterDelegateMulticast7.AddUObject(UObjMC, &UDelegatepTestClass::DelegateProc1);

其他的绑定方式差不太多,故在此不赘述。

所有的绑定方式如下:

函数名 用途
FDelegateHandle Add(const FDelegate& InNewDelegate) 将某个函数委托添加到该多播委托的调用列表中
FDelegateHandle AddStatic(...) 添加原始C++指针全局函数委托
FDelegateHandle AddLambda(...) 添加匿名函数委托
FDelegateHandle AddWeakLambda(...) 添加弱引用对象的匿名函数委托,会对对象弱引用
FDelegateHandle AddRaw(...) 添加原始C++指针委托。原始指针不使用任何类型的引用,因此如果从委托下面删除了对象,则调用此函数可能不安全。调用Execute()时请小心!
FDelegateHandle AddSP(...) 添加基于共享指针的(快速、非线程安全)成员函数委托,共享指针委托保留对对象的弱引用
FDelegateHandle AddThreadSafeSP(...) 添加基于共享指针的成员函数委托(相对较慢,但是线程安全),会对对象弱引用
FDelegateHandle AddUFunction(...) 添加UFunction类型的成员函数,会对输入的对象弱引用
FDelegateHandle AddUObject(...) 添加UObject对象的成员函数,会对输入的对象弱引用
2.1.3.c 执行

委托.Broadcast()即可,即便在没有任何绑定的时候都可以用这个函数来触发委托执行。不过需要注意的是,绑定函数的执行顺序是未定义的,执行顺序很可能与绑定顺序不同(毕竟多播委托可能会多次添加、移除委托)。

image-20211007222642171

2.1.3.d 底层实现
保存的绑定函数数组

先看宏定义:

image-20211007224453206

接着往下看:

image-20211007224518744

可以看到实际上是TMulticastDelegate,看看它的定义:

image-20211007224553521

和单播委托一样,通过偏特化的方式保证UserPolicy在有默认值的同时能够让用户输入自己定义的UserPolicy。也是和单播委托一样,实际上保存指针的数组并不在TMulticastDelegate中,要在基类中查找,我们先看上一级的UserPolicy::FMulticastDelegateExtras,即TMulticastDelegateBase<fdefaultdelegateuserpolicy>

image-20211007224840340

可以看到,实际上就是一个TDelegateBase数组(FMulticastInvocationListAllocatorType先不用管,主要是和内存分配有关,与我们关注的重点不太相关)。

其实说到这里基本上可以和单播委托那边的分析结合起来看,但是首先,我们先接着看绑定的实现。

绑定的实现

首先我们看看常用的AddUObject是怎么实现的:

template <typename userclass,="" typename...="" vartypes="">    inline FDelegateHandle AddUObject(UserClass* InUserObject, typename TMemFunPtrType<false, userclass,="" void="" (paramtypes...,="" vartypes...)="">::Type InFunc, VarTypes... Vars)    {        static_assert(!TIsConst<userclass>::Value, "Attempting to bind a delegate with a const object pointer and non-const member function.");      // 这里实际上调用了上面提到的FDelegate::CreateUObject,不理解的话看上面的内容即可        return Add(FDelegate::CreateUObject(InUserObject, InFunc, Vars...));    }

可以看到实际上还是依靠了另一个函数Add,并且实际上用到了上面提到的单播委托的FDelegate::CreateUObject来创建一个委托对象。那么我们接着看看Add的实现:

/**     * Adds a delegate instance to this multicast delegate's invocation list.     *     * @param Delegate The delegate to add.     */    FDelegateHandle Add(FDelegate&& InNewDelegate)    {        FDelegateHandle Result;        if (Super::GetDelegateInstanceProtectedHelper(InNewDelegate))        {            Result = Super::AddDelegateInstance(MoveTemp(InNewDelegate));        }        return Result;    }

这里的Super其实是:

image-20211007230828611

TMulticastDelegateBase<fdefaultdelegateuserpolicy>,因此最终会调用TMulticastDelegateBase的:

image-20211007230934442

InvocationList的定义是:

image-20211007231039524

即用到上面定义的类型:

image-20211007231102427

所以实际上就是先创建一个单播委托,然后添加到了自己维护的TArray数组中。

执行委托的实现
/**     * Broadcasts this delegate to all bound objects, except to those that may have expired.     *     * The constness of this method is a lie, but it allows for broadcasting from const functions.     */    void Broadcast(ParamTypes... Params) const    {        bool NeedsCompaction = false;        Super::LockInvocationList();        {            const InvocationListType& LocalInvocationList = Super::GetInvocationList();            // call bound functions in reverse order, so we ignore any instances that may be added by callees            for (int32 InvocationListIndex = LocalInvocationList.Num() - 1; InvocationListIndex >= 0; --InvocationListIndex)            {                // this down-cast is OK! allows for managing invocation list in the base class without requiring virtual functions                const FDelegate& DelegateBase = (const FDelegate&)LocalInvocationList[InvocationListIndex];                IDelegateInstance* DelegateInstanceInterface = Super::GetDelegateInstanceProtectedHelper(DelegateBase);                if (DelegateInstanceInterface == nullptr || !((DelegateInstanceInterfaceType*)DelegateInstanceInterface)->ExecuteIfSafe(Params...))                {                    NeedsCompaction = true;                }            }        }        Super::UnlockInvocationList();        if (NeedsCompaction)        {            const_cast<tmulticastdelegate*>(this)->CompactInvocationList();        }    }

可以看到实际上就是遍历一遍数组然后一个个调用ExecuteIfSafe(传入参数)。注意ExecuteIfSafe,如果委托无法被执行,那么就会返回false

ExecuteIfSafe的实现随着不同类型的绑定函数而不同,例如如果绑定的是全局函数,实际上实现是:

bool ExecuteIfSafe(ParamTypes... Params) const final{  // Call the static function  checkSlow(StaticFuncPtr != nullptr);  (void)this->Payload.ApplyAfter(StaticFuncPtr, Params...);  return true;}

可以看到无论如何都会执行,但是如果是别的,例如绑定的是weaklambda,那么:

bool ExecuteIfSafe(ParamTypes... Params) const final{  if (ContextObject.IsValid())  {    (void)this->Payload.ApplyAfter(Functor, Params...);    return true;  }  return false;}

会判断弱引用的对象是不是还有效,如果已经被销毁了就不会执行并且返回false

这样就可以保证无论何时调用Broadcast()都是安全的。

2.1.3.e 总结
  1. 实际上多播委托就是维护了一个由单播委托组成的数组,依托单播委托实现的;

  2. 无论何时调用Broadcast()都是安全的。


2.1.4 事件

事件和多播委托相似(实际上就是多播,只是多了个friend class OwningType,用来辨别调用者是不是代理拥有者),功能都差不多,只是限定死了部分函数的权限:只有声明事件的类可以调用事件的BroadcastIsBoundClear函数。这就保证了只有事件的拥有者能够触发事件。

事件绑定的函数也是不能够有返回值的。

// 和组播类似// 注意首个参数,用来指定事件拥有者DECLARE_EVENT( OwningType, EventName );// 1个参数DECLARE_EVENT_OneParam( OwningType, EventName, Param1Type );// 2个参数DECLARE_EVENT_TwoParams( OwningType, EventName, Param1Type, Param2Type );// 多个参数DECLARE_EVENT_<num>Params( OwningType, EventName, Param1Type, Param2Type, ...);

事件和多播基本一致,而且因为后面的版本中事件类型会被移除,因此这里不进行详细说明。

2.1.5 动态多播委托

实际上上面已经详细说明了动态委托、多播委托,如果上面的内容理解了的话那么这里的内容也是很容易能够理解的了。

2.1.5.a 声明
// 动态多播不能有返回值,所以只列举有参数、无参数的例子// 无参数DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOpenViewDelegate_DynamicMulticast);// 1个参数,和前面不同的是要加上参数名字DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FCharacterDelegate_DynamicMulticast, int, nCode); 
2.1.5.b 绑定
绑定函数 使用场景
Add 添加一个函数委托
AddUnique 添加一个函数委托,但是只有在这个函数委托不存在维护的数组中的时候才添加(根据委托的签名是否已经存在数组中进行判断)
AddDynamic 用来绑定一个UObject类型的成员函数到委托中(这个接口实际上通过宏重定向到__Internal_AddDynamic
AddUniqueDynamic 与上面的AddDynamic一样,但是会根据函数的签名确保不重复添加
2.1.5.c 执行

直接调用Broadcast(输入参数)即可,任何时候都可以调用这个函数,与多播委托一样。

2.1.5.d 底层实现
/** Declares a blueprint-accessible broadcast delegate that can bind to multiple native UFUNCTIONs simultaneously */#define DECLARE_DYNAMIC_MULTICAST_DELEGATE( DelegateName ) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_DELEGATE) FUNC_DECLARE_DYNAMIC_MULTICAST_DELEGATE( FWeakObjectPtr, DelegateName, DelegateName##_DelegateWrapper, , FUNC_CONCAT( *this ), void )

可以看出实际上是调用了FUNC_DECLARE_DYNAMIC_MULTICAST_DELEGATE:

/** Declare user's dynamic multi-cast delegate, with wrapper proxy method for executing the delegate */#define FUNC_DECLARE_DYNAMIC_MULTICAST_DELEGATE(TWeakPtr, DynamicMulticastDelegateClassName, ExecFunction, FuncParamList, FuncParamPassThru, ...) \class DynamicMulticastDelegateClassName : public TBaseDynamicMulticastDelegate<tweakptr, __va_args__=""> \    { \    public: \        /** Default constructor */ \        DynamicMulticastDelegateClassName() \        { \        } \        \        /** Construction from an FMulticastScriptDelegate must be explicit.  This is really only used by UObject system internals. */ \        explicit DynamicMulticastDelegateClassName( const TMulticastScriptDelegate<>& InMulticastScriptDelegate ) \            : TBaseDynamicMulticastDelegate<tweakptr, __va_args__="">( InMulticastScriptDelegate ) \        { \        } \        \        /** Broadcasts this delegate to all bound objects, except to those that may have expired */ \        void Broadcast( FuncParamList ) const \        { \            ExecFunction( FuncParamPassThru ); \        } \    };

可以看到,实际上和动态委托类似,会变成一个继承TBaseDynamicMulticastDelegate的类:

image-20211008002920696

TBaseDynamicMulticastDelegate提供了__Internal_AddDynamic的实现:

/**     * Binds a UObject instance and a UObject method address to this multi-cast delegate.     *     * @param    InUserObject        UObject instance     * @param    InMethodPtr            Member function address pointer     * @param    InFunctionName        Name of member function, without class name     *     * NOTE:  Do not call this function directly.  Instead, call AddDynamic() which is a macro proxy function that     *        automatically sets the function name string for the caller.     */    template< class UserClass >    void __Internal_AddDynamic( UserClass* InUserObject, typename FDelegate::template TMethodPtrResolver< UserClass >::FMethodPtr InMethodPtr, FName InFunctionName )    {        check( InUserObject != nullptr && InMethodPtr != nullptr );        // NOTE: We're not actually storing the incoming method pointer or calling it.  We simply require it for type-safety reasons.        FDelegate NewDelegate;        NewDelegate.__Internal_BindDynamic( InUserObject, InMethodPtr, InFunctionName );        this->Add( NewDelegate );    }

最终调用的Add则由基类TMulticastScriptDelegate实现:

image-20211008003104051

而且最终保存的数组实际上也保存在TMulticastScriptDelegate中:

image-20211008003145768

可以看到,实际上就是一个数组,里面保存了一系列的动态委托。而Broadcast(传入参数)最终会调用到TMulticastScriptDelegate的:

/**     * Executes a multi-cast delegate by calling all functions on objects bound to the delegate.  Always     * safe to call, even if when no objects are bound, or if objects have expired.  In general, you should     * never call this function directly.  Instead, call Broadcast() on a derived class.     *     * @param    Params                Parameter structure     */    template <class uobjecttemplate="">    void ProcessMulticastDelegate(void* Parameters) const    {        if( InvocationList.Num() > 0 )        {            // Create a copy of the invocation list, just in case the list is modified by one of the callbacks during the broadcast            typedef TArray< TScriptDelegate<tweakptr>, TInlineAllocator< 4 > > FInlineInvocationList;            FInlineInvocationList InvocationListCopy = FInlineInvocationList(InvocationList);                // Invoke each bound function            for( typename FInlineInvocationList::TConstIterator FunctionIt( InvocationListCopy ); FunctionIt; ++FunctionIt )            {                if( FunctionIt->IsBound() )                {                    // Invoke this delegate!                    FunctionIt->template ProcessDelegate<uobjecttemplate>(Parameters);                }                else if ( FunctionIt->IsCompactable() )                {                    // Function couldn't be executed, so remove it.  Note that because the original list could have been modified by one of the callbacks, we have to search for the function to remove here.                    RemoveInternal( *FunctionIt );                }            }        }    }

与多播委托类似,也是会在调用前先用FunctionIt->IsBound()进行判断,确保执行安全。当然,前面提到了动态委托运行速度很慢,所以您可以猜到动态多播会是本文中所有的委托中执行最慢的。

参考

注意:因为文章经过多次修改,因此实际上这里的顺序与文中提及的顺序不一致。LaTeX对引用顺序的处理就很好,所以后面我可能会考虑改用LaTeX来做这类笔记


  1. UE 4.26源代码 ↩︎

  2. 官方文档:委托:严重过时的官方文档,请以最新源代码内容为准 ↩︎ ↩︎

  3. 关于各类委托之间的不同点的讨论 ↩︎

  4. C++中实现委托:如果好奇在纯C++代码中如何实现委托,那么可以参考这篇文章 ↩︎

  5. 全面理解UE4委托 ↩︎

  6. UE4:4.26版本对Delegate模块的改进 ↩︎

  7. C++ 模板,特化,与偏特化 ↩︎

  8. 泛化之美–C++11可变模版参数的妙用 ↩︎

  9. UE4-深入委托Delegate实现原理:这篇文章可以说是帮了大忙,不过本文部分内容实际上参考了这里的分析。但是文中有一部分内容已经对应不上4.26及以后的版本的源代码了。不过,还是非常值得一看,强烈推荐
    </tweakptr,></tweakptr,></tmulticastdelegate*></false,></vartypes…></functype,></typename…></functype,></retvaltype(paramtypes…),></retvaltype(paramtypes…),></functype,></retvaltype(paramtypes…),></retvaltype(paramtypes…),></typename…></inretvaltype(paramtypes…),></returntype(一堆您传入的参数)> ↩︎

  10. FFuncPtr官方文档:这个官方文档和往常一样,写得和没写一样,建议看别的 ↩︎

  11. 【UE4笔记】各种Delegate委托的区别和应用 ↩︎