1. 程式人生 > 其它 >UE4技術總結——委託

UE4技術總結——委託

主要是關於UE4中委託的實現

UE4技術總結——委託

目錄

在開始前先說明,這部分內容主要基於UE 4.26.2版本[1]編寫,因此讀者可能需要注意時效性。如果您對原始碼感興趣,可以直接閱讀引擎的DelegateCombinations.hDelegate.h以及相關程式碼。

因為是一個非常基礎,時不時會用到的功能,所以這裡就不介紹使用場景了,直接進入正題。

一、定義

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

委託 是一種泛型但型別安全的方式,可在C++物件上呼叫成員函式。可使用委託動態繫結到任意物件的成員函式,之後在該物件上呼叫函式,即使呼叫程式不知物件型別也可進行操作。複製委託物件很安全。你也可以利用值傳遞委託,但這樣操作需要在堆上分配記憶體,因此通常並不推薦。請儘量通過引用

傳遞委託。

同時,根據官方文件,虛幻引擎支援3種類型的委託:

  1. 單播委託
  2. 多播委託
    1. 事件
  3. 動態委託

之所以說是3種,是因為事件實際上在現在的版本中差不多就是多播委託(當然,實際上還是有些許不同的,主要是函式呼叫許可權和多播不同,但是實際上也沒有措施保證函式被不是擁有者的物件呼叫,因此讀者只需要理解為多播委託即可)[3]。而且在UE的4.26.2版本原始碼中已經標明,事件型別的委託將會在後面更新的版本移除掉:

因此,我們主要重點還是放在單播、多播、動態委託上,事件不會進行詳細說明。

同時,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,實際上可以是別的UObject
SingleDelagateWithNoParam.BindUObject(this, &ADelegateListener::EnableLight)

下面這張圖列舉了除了BindUObject之外還能夠使用什麼函式進行繫結,以及在什麼情況下使用[2:1]

除了BindUObject之外還有別的繫結函式,這裡直接借用官網過時的文件中的列表:

大概如上,都非常簡單,在使用的時候按照您要繫結的函式來選擇對應的函式來繫結即可。這裡簡單補充幾個官網文件沒有提及的繫結:

函式 描述
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(傳入引數)

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);

// PayLoad
FLearnDelegate DelegateObj2;
// 這裡的“TEST”會在呼叫繫結函式的時候緊接著委託對戲那個傳入的引數傳入
DelegateObj2.BindStatic(LearningDelegate, FString(TEXT("TEST!")));
// “TEST”會接在23.0f後面,所以最後是傳入到Test引數中
DelegateObj2.ExecuteIfBound(23.0f);
2.1.1.e 底層實現
繫結函式指標

相關程式碼在DelegateCombination.h以及Delegate.h中。

首先我們需要有個大體的概念,其實本質上就是儲存了一個函式指標,在執行的時候直接訪問該函式指標對應的函式即可,如果是成員函式則比較特殊,需要同時知道成員函式所在的類,同時應該有一個指標指向該物件。接下來我們看具體實現。

可以看到,實際上就是通過TDelegate這個類來實現的,所以實際上我們在定義委託的時候就是在呼叫TDelegate<returntype(一堆您傳入的引數)>建立委託型別,並通過typedef重新命名為您給定的名字,方便記憶與閱讀。TDelegateDelegateSignatureImpl.ini中實現。因為內容比較多,因此我們只看關鍵部分。首先我們看到他繼承了TDelegateBase這個類:

簡單掃幾眼,就會發現實際上用來儲存指向函式的指標並不在TDelegate中,而應該是放在了父類,也就是TDelegateBase

讀者可能發現了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],能夠在給定預設值的同時,能夠讓使用者輸入自己希望的值):

因此實際上此處的UserPolicyFDefaultDelegateUserPolicy,那麼我們簡單看看FDefaultDelegateUserPolicy這一struct的內容:

struct FDefaultDelegateUserPolicy
{
    // 這裡的using是別名指定
    using FDelegateInstanceExtras  = IDelegateInstance;
    // 注意下面這個,另外兩個會在其他委託中用到,先不管
    using FDelegateExtras          = FDelegateBase;
    using FMulticastDelegateExtras = TMulticastDelegateBase<fdefaultdelegateuserpolicy>;
};

回到開始的TDelegate<inretvaltype(paramtypes...), userpolicy=""> : public TDelegateBase<userpolicy>,我們看看TDelegateBase的定義:

所以實際上最終還是繼承了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

可以很輕鬆找到儲存C++函式指標的變數(這個變數型別是UE4提供的專門用來儲存C++函式指標的型別,網上資料很多[10],這裡就不進行介紹了)。

同理,相似的,繫結UObject物件成員函式委託建立函式則有:

最終執行的時候的形式就類似於這樣:

// 全劇函式
(*MyDelegate)();
// 物件成員函式
(MyObj->*FuncPtr)();
// 如果是在棧上
(StackObj.*Func1Ptr)();
Payload的實現

當然實際上UE4中會支援Payload,會先把一部分預先輸入的引數拼接到呼叫委託的時候傳入的引數後面去,形成一個引數列表,最後一起作為引數輸入到繫結函式,但是原理差不多。

以全域性函式的執行為例:

Payload實際上是一個TTuple

最終執行:

因為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中的可變模板引數實現)。

最終還是:

另外,這裡的__VA_ARGS__實際上就是:

比較容易理解,所以這裡不作詳細解釋。

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 執行委託

和單播委託類似:

2.1.2.d 底層實現

可以看到實際上依託TBaseDynamicDelegate來實現,而且巨集定義宣告一個動態委託就是聲明瞭一個類繼承TBaseDynamicDelegate。巨集定義裡面也另外定義了ExecuteIfBoundExecute函式,實際執行委託也是通過巨集定義裡面定義的這兩個函式,同時依託UE4的反射、序列化機制實現的。

TBaseDynamicDelegate內的實現不多,實際上還是得依靠TScriptDelegate

TScriptDelegate才是真正儲存函式名字、繫結的物件的弱指標的地方:

簡單看下繫結部分,因為只能繫結UFUNCTION函式,所以只有一個繫結函式:

執行則是依託一開始巨集定義裡面定義的Execute(傳入引數)

實際執行的時候UE4會根據輸入的函式名字找到對應的函式並執行,這個函式最終會被上面定義的Execute呼叫:

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()即可,即便在沒有任何繫結的時候都可以用這個函式來觸發委託執行。不過需要注意的是,繫結函式的執行順序是未定義的,執行順序很可能與繫結順序不同(畢竟多播委託可能會多次新增、移除委託)。

2.1.3.d 底層實現
儲存的繫結函式陣列

先看巨集定義:

接著往下看:

可以看到實際上是TMulticastDelegate,看看它的定義:

和單播委託一樣,通過偏特化的方式保證UserPolicy在有預設值的同時能夠讓使用者輸入自己定義的UserPolicy。也是和單播委託一樣,實際上儲存指標的陣列並不在TMulticastDelegate中,要在基類中查詢,我們先看上一級的UserPolicy::FMulticastDelegateExtras,即TMulticastDelegateBase<fdefaultdelegateuserpolicy>

可以看到,實際上就是一個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其實是:

TMulticastDelegateBase<fdefaultdelegateuserpolicy>,因此最終會呼叫TMulticastDelegateBase的:

InvocationList的定義是:

即用到上面定義的型別:

所以實際上就是先建立一個單播委託,然後新增到了自己維護的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的類:

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實現:

而且最終儲存的陣列實際上也儲存在TMulticastScriptDelegate中:

可以看到,實際上就是一個數組,裡面儲存了一系列的動態委託。而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委託的區別和應用 ↩︎

本部落格文章預設使用CC BY-SA 3.0協議。