この記事では、Unreal Engine の C++ でデリゲートを扱う方法についてまとめています。
執筆時に扱った Unreal Engine のソースコードのバージョンは 4.25.4
プラットフォームは Windows 10 (64ビット版) を対象としています。
ビルドに使用したアプリ(IDE)は Microsoft Visual Studio 2019 です。
以降、Unreal Engine 4 を UE4 と略記します。
現在、Unreal Engine 5(UE5) での変更点を以下の環境で確認中です。
UE 5.5.3
Windows 11
Visual Studio 2022
確認がとれたものに関しては記事中に追記しています。
更新履歴
2025/03/10
UE5 でスパースデリゲートはどうなったのか?
ダイナミック・シングルキャスト・デリゲート専用(UE5)
ダイナミック・マルチキャスト・デリゲート専用(UE5)を追記。
2025/03/09
UE5 でイベントデリゲートはどうなったのか?を追記、もくじを追加。
2025/03/07
説明文とサンプルコードを一部修正。
- デリゲートの種類
- デリゲートを宣言できる場所
- デリゲートの宣言方法
- デリゲートのバインド方法
- ドキュメントと実装内容の相違点(イベント・デリゲート)
- UE5 でスパースデリゲートはどうなったのか?
UE4 のデリゲートは以下の4種類あります。
- シングルキャスト
- マルチキャスト
- ダイナミック
- イベント
以下でそれぞれについて説明します。
任意の関数ひとつをデリゲートにバインド(結びつけ)できます。
複数の関数をデリゲートにバインドでき、バインドしたすべての関数を一度に呼び出すことができます。
デリゲートはシリアライズされ、リフレクションを使って関数を呼び出すことができます。
マルチキャストに近いですが、任意のクラス内で定義して使います。
マルチキャストと同様に、バインドしたすべての関数を一度に呼び出すことができます。
デリゲートは以下の3つのスコープで宣言することができます。
- グローバル・スコープ
- namespace の中
- クラスの中
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" // 1. グローバル・スコープ namespace ほげほげ { // 2. namespace の中 } UCLASS() class もげ { GENERATED_BODY() public: か protected: か private: // 3. クラスの中 }; namespace ほげ~ん { UCLASS() class もげら { GENERATED_BODY() public: か protected: か private: // 2. + 3. namespace の中にあるクラスの中 }; }
専用のマクロを使って、デリゲートを宣言します。
デリゲートに登録する関数は、戻り値あり/なしに対応していて、引数の数は0個~9個まで持つことができます。
具体的な宣言文を以下で紹介していきます。
DECLARE_DELEGATE(デリゲート名);
例
// 戻り値なし、引数なし関数をバインドできるデリゲートの宣言 DECLARE_DELEGATE(FSampleDelegate);
DECLARE_DELEGATE_OneParam(デリゲート名,引数1の型);
例
// 戻り値なし、引数1個の関数をバインドできるデリゲートの宣言 DECLARE_DELEGATE_OneParam(FSampleDelegate, const int32);
DECLARE_DELEGATE_TwoParams(デリゲート名,引数1の型,引数2の型);
DECLARE_DELEGATE_ThreeParams(デリゲート名,引数1の型,引数2の型,引数3の型);
DECLARE_DELEGATE_FourParams(デリゲート名,略…);
DECLARE_DELEGATE_FiveParams(デリゲート名,略…);
DECLARE_DELEGATE_SixParams(デリゲート名,略…);
DECLARE_DELEGATE_SevenParams(デリゲート名,略…);
DECLARE_DELEGATE_EightParams(デリゲート名,略…);
DECLARE_DELEGATE_NineParams(デリゲート名,略…);
例
// 戻り値なし、引数4個の関数をバインドできるデリゲートの宣言 DECLARE_DELEGATE_FourParams(FSampleDelegate, struct FTest&, const int32, const float, const class USampleActor&);
DECLARE_DELEGATE_RetVal(戻り値の型,デリゲート名)
DECLARE_DELEGATE_RetVal_OneParam(戻り値の型,デリゲート名,引数1の型);
DECLARE_DELEGATE_RetVal_TwoParams(戻り値の型,デリゲート名,引数1の型,引数2の型);
DECLARE_DELEGATE_RetVal_ThreeParams(戻り値の型,デリゲート名,引数1の型,引数2の型,引数3の型);
DECLARE_DELEGATE_RetVal_FourParams(戻り値の型,デリゲート名,略…);
DECLARE_DELEGATE_RetVal_FiveParams(戻り値の型,デリゲート名,略…);
DECLARE_DELEGATE_RetVal_SixParams(戻り値の型,デリゲート名,略…);
DECLARE_DELEGATE_RetVal_SevenParams(戻り値の型,デリゲート名,略…);
DECLARE_DELEGATE_RetVal_EightParams(戻り値の型,デリゲート名,略…);
DECLARE_DELEGATE_RetVal_NineParams(戻り値の型,デリゲート名,略…);
シングルキャスト・デリゲートに関数ポインタをバインドする(結びつける)方法です。
バインドすることで、関数を呼び出せるようになります。
どのクラスにも属していない C++ のグローバルな static 関数をデリゲートにバインドします。
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Sample.generated.h" DECLARE_DELEGATE(FSampleDelegate); static void StaticSampleFunction() { UE_LOG(LogTemp, Log, TEXT("called StaticSampleFunction()")); } UCLASS() class USampleClass : public UObject { GENERATED_BODY() public: void Register() { SampleDelegate.BindStatic(StaticSampleFunction); } void Invoke() { SampleDelegate.ExecuteIfBound(); } private: FSampleDelegate SampleDelegate; };
C++ のラムダ式をデリゲートにバインドします。
厳密に言うと、ラムダ式以外のファンクタをバインドしても動作しますが、ラムダ式で使うことを主目的としています。
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Sample.generated.h" DECLARE_DELEGATE_RetVal_OneParam(int, FSampleDelegate, int); UCLASS() class USample : public UObject { GENERATED_BODY() public: void Register() { SampleDelegate.BindLambda( [](int a){ UE_LOG(LogTemp, Log, TEXT("called Lambda(%d)"), a); a += 10; return a; }); } void Invoke() { if (SampleDelegate.IsBound() == false) { return; } int ReturnedValue = SampleDelegate.Execute(Count); UE_LOG(LogTemp, Log, Text("ReturnedValue=%d"), ReturnedValue); } private: FSampleDelegate SampleDelegate; int Count = 0; };
C++ の生ポインタをデリゲートにバインドします。
生ポインタは参照されているかどうかを考慮しないので、オブジェクトが削除された後にデリゲートが呼ばれた場合、安全ではない可能性があります。デリゲートを実行するときは注意が必要です。
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Sample.generated.h" DECLARE_DELEGATE_TwoParams(FSampleDelegate, int, float); class FBindRawSampleClass { public: void OnDelegateCalled(int a, float b) { UE_LOG(LogTemp, Log, TEXT("called OnDelegateCalled(%d, %f)"), a, b); } }; UCLASS() class USample : public UObject { GENERATED_BODY() public: USampleClass() { BindRawSample = new FBindRawSampleClass(); } ~USampleClass() { delete BindRawSample; } void Register() { SampleDelegate.BindRaw(BindRawSample, &FBindRawSampleClass::OnDelegateCalled); } void Invoke() { SampleDelegate.ExecuteIfBound(123, 3.14f); } private: FSampleDelegate SampleDelegate; FBindRawSampleClass* BindRawSample = nullptr; };
シェアードリファレンスをデリゲートにバインドします。
具体的には TSharedRef のインスタンスか TSharedPtr::ToSharedRef() を渡します。
これは UE のガーベジコレクタで管理していないオブジェクトのポインタをバインドするためのものです。
この関数はスレッドセーフではありません。
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Sample.generated.h" DECLARE_DELEGATE(FSampleDelegate); UCLASS() class USampleClass : public UObject { GENERATED_BODY() public: USampleClass() { BindSPSample = MakeShareable(new FBindSPSampleClass()); } void Register() { SampleDelegate.BindSP(BindSPSample.ToSharedRef(), &FBindSPSampleClass::OnDelegateCalled); } void Invoke() { SampleDelegate.ExecuteIfBound(123, 3.14f); } private: FSampleDelegate SampleDelegate; TSharedPtr<class FBindSPSampleClass> BindSPSample = nullptr; }; class FBindSPSampleClass { public: void OnDelegateCalled() { UE_LOG(LogTemp, Log, TEXT("called OnDelegateCalled()")); } };
BindSP() のスレッドセーフ版です。
スレッドセーフ版は並列処理に対する安全性は高まりますが、速度が犠牲になるため、確実に必要になる場合にのみ使用します。
UObject を継承しているクラスのメンバ関数をデリゲートにバインドします。
UObject を継承していないクラスのポインタを指定するとコンパイルエラーが出ます。
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Sample.generated.h" DECLARE_DELEGATE(FSampleDelegate); UCLASS() class USample : public UObject { GENERATED_BODY() public: void Register() { SampleDelegate.BindUObject(this, &USampleClass::OnDelegateCalled); } void Invoke() const { SampleDelegate.ExecuteIfBound(); } private: FSampleDelegate SampleDelegate; void OnDelegateCalled() { UE_LOG(LogTemp, Log, TEXT("called OnDelegateCalled()")); } };
バインド関数の中で唯一、メンバ関数に文字列を使ってバインドできる方法です。
その代わり、バインドするメンバ関数には、UFUNCTION() マクロを使ってリフレクション対象にする必要があります。
UFUNCTION() マクロを使うメンバ関数を持つクラスは、 UCLASS() マクロを使っている必要があり、UCLASS() マクロを使うには UObject を継承する必要があります。
なので、バインドするクラスのポインタは UObject を継承している必要があります。
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Sample.generated.h" DECLARE_DELEGATE_OneParam(FSampleDelegate, int32); UCLASS() class USample : public UObject { GENERATED_BODY() public: void Register() { auto FunctionName = GET_FUNCTION_NAME_CHECKED(USampleClass, OnDelegateCalled); SampleDelegate.BindUFunction(this, FunctionName); } void Invoke() const { SampleDelegate.ExecuteIfBound(12345); } private: FSampleDelegate SampleDelegate; UFUNCTION() void OnDelegateCalled(int32 a) { UE_LOG(LogTemp, Log, TEXT("called OnDelegateCalled(%d)"), a); } };
バージョン 4.21 で追加された関数ですが、使い道がよく分かりません。
使い方を理解するためにいくつかサンプルコードを作りましたが、この関数を使わず BindLambda() でバインドした場合でも、Execute() を実行する前に IsBound() でチェックするか、ExecuteIfBound() を使えばクラッシュを防ぐことができました。
なので、どういう状況でこのバインド関数を使う必要があるのか?が分かりません。
ブループリント用でしょうか…?
これまでに記載した「使用例」を見ていただければお分かりいただけると思いますが、バインド関数の3番目の引数「引数リスト」は一度も使用していません。
使用例でこの引数を使わなかったのは、先にこの引数リストの使い方を例示してしまうと、UE のデリゲートについてよく知らない人が混乱しそうだからです。
長々と説明するより、コードを見ていただく方が早いので、以下に使用例を記載します。
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Sample.generated.h" // 戻り値と引数を持たないデリゲートを宣言 DECLARE_DELEGATE(FSampleDelegate); UCLASS() class USample : public UObject { GENERATED_BODY() public: void Register() { // バインド関数に引数を2つ指定 SampleDelegate.BindUFunction(this, "OnDelegateCalled", 1, 2); } void Invoke() const { // デリゲートを実行する際に引数を指定していない // OnDelegateCalled() として呼び出された場合、引数の数が違うので // コンパイルエラーになりそうだが、バインド関数に指定した引数が使われるため // OnDelegateCalled(1, 2) として呼び出される。 SampleDelegate.ExecuteIfBound(); } private: FSampleDelegate SampleDelegate; // 実際に呼び出される関数は引数を2つ持っている UFUNCTION() void OnDelegateCalled(int32 a, int32 b) { UE_LOG(LogTemp, Log, TEXT("called OnDelegateCalled(%d, %d)"), a, b); } };
DECLARE_DELEGATE(FSampleDelegate); 戻り値も引数もないデリゲートとして宣言しているのですが、バインドしている関数(OnDelegateCalled)は2つの引数を持っています。
このように、宣言とは異なる数の引数を渡すことができます。
増やした分の引数を、バインド関数の「引数リスト」に渡すことができます。
逆の使い方もできます。
以下で例示します。
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Sample.generated.h" // 戻り値なし、2つの引数を持つデリゲートとして宣言 DECLARE_DELEGATE_TwoParams(FSampleDelegate, int32, int32); UCLASS() class USample : public UObject { GENERATED_BODY() public: void Register() { // OnDelegateCalled() は引数を1つしか持っていませんがバインドできます。 SampleDelegate.BindUFunction(this, "OnDelegateCalled"); } void Invoke() const { // 問題なく呼び出せますが、2番目の引数は無視されます。 SampleDelegate.ExecuteIfBound(888, 999); } private: FSampleDelegate SampleDelegate; UFUNCTION() void OnDelegateCalled(int32 a) { UE_LOG(LogTemp, Log, TEXT("called OnDelegateCalled(%d)"), a); } };
引数の数は重要ではないようです。
DECLARE_MULTICAST_DELEGATE(デリケート名)
DECLARE_MULTICAST_DELEGATE_OneParam(デリケート名, 引数1の型)
DECLARE_MULTICAST_DELEGATE_TwoParams(デリケート名, 引数1の型, 略…)
DECLARE_MULTICAST_DELEGATE_ThreeParams(略)
DECLARE_MULTICAST_DELEGATE_FourParams(略)
DECLARE_MULTICAST_DELEGATE_FiveParams(略)
DECLARE_MULTICAST_DELEGATE_SixParams(略)
DECLARE_MULTICAST_DELEGATE_SevenParams(略)
DECLARE_MULTICAST_DELEGATE_EightParams(略)
DECLARE_MULTICAST_DELEGATE_NineParams(略)
DECLARE_MULTICAST_DELEGATE_RetVal(戻り値の型, デリケート名)
DECLARE_MULTICAST_DELEGATE_RetVal_OneParam(戻り値の型, デリケート名, 引数1の型)
DECLARE_MULTICAST_DELEGATE_RetVal_TwoParams(戻り値の型, デリケート名, 引数1の型, 略…)
DECLARE_MULTICAST_DELEGATE_RetVal_ThreeParams(略)
DECLARE_MULTICAST_DELEGATE_RetVal_FourParams(略)
DECLARE_MULTICAST_DELEGATE_RetVal_FiveParams(略)
DECLARE_MULTICAST_DELEGATE_RetVal_SixParams(略)
DECLARE_MULTICAST_DELEGATE_RetVal_SevenParams(略)
DECLARE_MULTICAST_DELEGATE_RetVal_EightParams(略)
DECLARE_MULTICAST_DELEGATE_RetVal_NineParams(略)
シングルキャスト・デリゲートと違い、バインド関数ではなく、追加関数(Add*)を使います。
追加関数によって、呼び出すデリゲートを実行リスト(invocation list)にひとつずつ登録します。
リストに登録したデリゲートは、Broadcast() 関数でまとめて実行します。
以下の関数は実行リストにデリゲートを追加する関数です。
追加関数は FDelegateHandle を戻り値として返し、そのハンドルはデリゲートを削除するときに使います。
Add(デリゲートのインスタンス)
AddStatic(グローバルな C++ のスタティック関数のポインタ, 引数リスト)
AddRaw(オブジェクトのポインタ, メンバ関数のポインタ, 引数リスト)
AddSP(シェアードリファレンス, メンバ関数のポインタ, 引数リスト)
AddUObject(UObject継承クラスのポインタ, メンバ関数のポインタ, 引数リスト)
以下の関数は実行リストに登録したデリゲートをリストから削除するために使います。
特定のデリゲートだけを削除
Remove(FDelegateHandle)
全てのデリゲートを削除
RemoveAll()
以上。
実行リストにデリゲートが登録されている場合、true を返します。
IsBound()
指定したオブジェクトのデリゲートが実行リストに登録されている場合、true を返します。
IsBoundToObject(オブジェクトのポインタ)
実行リストに登録したデリゲートを全て実行
実行リストに登録したデリゲートが呼び出される順番は未定義です。
Broadcast()
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Sample.generated.h" DECLARE_MULTICAST_DELEGATE_OneParam(FSampleDelegate, int); static void SampleStaticFunction(int a, int b) { UE_LOG(LogTemp, Log, TEXT("called SampleStaticFunction(%d) : %d"), a * b, b); } class FSampleRawClass { public: void OnRawDelegate(int a, int b) { UE_LOG(LogTemp, Log, TEXT("called FSampleRawClass::OnRawDelegate(%d) : %d"), a * b, b); } }; UCLASS() class USample : public UObject { GENERATED_BODY() public: USample() { FDelegateHandle h1 = SampleDelegate.AddLambda([](int a, int b) { UE_LOG(LogTemp, Log, TEXT("called LambdaA(%d) : %d"), a + b, b); }, 1); FDelegateHandle h2 = SampleDelegate.AddLambda([](int a, int b) { UE_LOG(LogTemp, Log, TEXT("called LambdaB(%d) : %d"), a + b, b); }, 2); FDelegateHandle h3 = SampleDelegate.AddUObject(this, &USample::OnDelegateCalled, 3); FDelegateHandle h4 = SampleDelegate.AddUObject(this, &USample::OnDelegateCalled, 4); FDelegateHandle h5 = SampleDelegate.AddUFunction(this, "OnUFunctionDelegate", 5); FDelegateHandle h6 = SampleDelegate.AddUFunction(this, "OnUFunctionDelegate", 6); FDelegateHandle h7 = SampleDelegate.AddStatic(SampleStaticFunction, 7); auto f = TBaseDelegate<void, int>::CreateLambda([](int a, int b) { UE_LOG(LogTemp, Log, TEXT("called LambdaC(%d) : %d"), a + b, b); }, 8); FDelegateHandle h8 = SampleDelegate.Add(f); SampleRawClass = MakeShareable(new FSampleRawClass()); FDelegateHandle h9 = SampleDelegate.AddRaw(SampleRawClass.Get(), &FSampleRawClass::OnRawDelegate, 9); FDelegateHandle h10 = SampleDelegate.AddSP(SampleRawClass.ToSharedRef(), &FSampleRawClass::OnRawDelegate, 10); SampleDelegate.Remove(h1); SampleDelegate.Remove(h4); SampleDelegate.Remove(h6); } void Invoke() const { if (SampleDelegate.IsBoundToObject(this) == false) { UE_LOG(LogTemp, Log, TEXT("%p is not bound to SampleDelegate"), this); } if (SampleDelegate.IsBound() == false) { return; } SampleDelegate.Broadcast(100); } private: FSampleDelegate SampleDelegate; TSharedPtr<FSampleRawClass> SampleRawClass = nullptr; void OnUObjectDelegate(int a, int b) { UE_LOG(LogTemp, Log, TEXT("called OnUObjectDelegate(%d) : %d"), a - b, b); } UFUNCTION() void OnUFunctionDelegate(int a, int b) { UE_LOG(LogTemp, Log, TEXT("called OnUFunctionDelegate(%d) : %d"), a - b, b); } };
ダイナミック・デリゲートには、シングルキャストとマルチキャストの両方があります。
ダイナミック・デリゲートはシリアライズされるため、バインドしたデリゲートを関数名で検索できます。
ただし、通常のデリゲートよりも動作が遅くなります。
ダイナミック・シングルキャスト・デリゲート
DECLARE_DYNAMIC_DELEGATE(デリケート名)
DECLARE_DYNAMIC_DELEGATE_OneParam(デリケート名, 引数1の型)
DECLARE_DYNAMIC_DELEGATE_TwoParams(デリケート名, 引数1の型, 略…)
DECLARE_DYNAMIC_DELEGATE_ThreeParams(略)
DECLARE_DYNAMIC_DELEGATE_FourParams(略)
DECLARE_DYNAMIC_DELEGATE_FiveParams(略)
DECLARE_DYNAMIC_DELEGATE_SixParams(略)
DECLARE_DYNAMIC_DELEGATE_SevenParams(略)
DECLARE_DYNAMIC_DELEGATE_EightParams(略)
DECLARE_DYNAMIC_DELEGATE_NineParams(略)
DECLARE_DYNAMIC_DELEGATE_RetVal(戻り値の型, デリケート名)
DECLARE_DYNAMIC_DELEGATE_RetVal_OneParam(戻り値の型, デリケート名, 引数1の型)
DECLARE_DYNAMIC_DELEGATE_RetVal_TwoParams(戻り値の型, デリケート名, 引数1の型, 略…)
DECLARE_DYNAMIC_DELEGATE_RetVal_ThreeParams(略)
DECLARE_DYNAMIC_DELEGATE_RetVal_FourParams(略)
DECLARE_DYNAMIC_DELEGATE_RetVal_FiveParams(略)
DECLARE_DYNAMIC_DELEGATE_RetVal_SixParams(略)
DECLARE_DYNAMIC_DELEGATE_RetVal_SevenParams(略)
DECLARE_DYNAMIC_DELEGATE_RetVal_EightParams(略)
DECLARE_DYNAMIC_DELEGATE_RetVal_NineParams(略)
ダイナミック・マルチキャスト・デリゲート
DECLARE_DYNAMIC_MULTICAST_DELEGATE(デリケート名)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(デリケート名, 引数1の型)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(デリケート名, 引数1の型, 略…)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FiveParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_SixParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_SevenParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_EightParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_NineParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_RetVal(戻り値の型, デリケート名)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_RetVal_OneParam(戻り値の型, デリケート名, 引数1の型)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_RetVal_TwoParams(戻り値の型, デリケート名, 引数1の型, 略…)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_RetVal_ThreeParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_RetVal_FourParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_RetVal_FiveParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_RetVal_SixParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_RetVal_SevenParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_RetVal_EightParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_RetVal_NineParams(略)
バインド(シングルキャスト)と追加(マルチキャスト)はマクロを使うと便利です。
ダイナミック・シングルキャスト・デリゲート専用(UE4/UE5共通)
バインド用マクロ
BindDynamic(UObject継承クラスのポインタ, メンバ関数のポインタ)
実行関数
Execute()
ExecuteIfBound()
ダイナミック・マルチキャスト・デリゲート専用(UE4/UE5共通)
追加用マクロ
AddDynamic(UObject継承クラスのポインタ, メンバ関数のポインタ)
RemoveDynamic(UObject継承クラスのポインタ, メンバ関数のポインタ)
同じメンバ関数を複数回登録することは想定していないようです。
ただ、ensure() を無視してプログラムの実行を継続しても、クラッシュはしませんでした。
同じメンバ関数を重複登録する可能性がある場合は、AddUniqueDynamic を使うと良いです。
実行関数
Broadcast()
シングルでもマルチでも使える関数
IsBound()
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Sample.generated.h" DECLARE_DYNAMIC_MULTICAST_DELEGATE(FSampleDelegate); UCLASS() class USample : public UObject { GENERATED_BODY() public: USample() { SampleDelegate.AddDynamic(this, &USample::OnUFunctionDelegate1); SampleDelegate.AddDynamic(this, &USample::OnUFunctionDelegate2); SampleDelegate.RemoveDynamic(this, &USample::OnUFunctionDelegate1); } void Invoke() const { if (SampleDelegate.IsBound() == false) { return; } SampleDelegate.Broadcast(); } private: FSampleDelegate SampleDelegate; UFUNCTION() void OnUFunctionDelegate1() { UE_LOG(LogTemp, Log, TEXT("called OnUFunctionDelegate1()")); } UFUNCTION() void OnUFunctionDelegate2() { UE_LOG(LogTemp, Log, TEXT("called OnUFunctionDelegate2()")); } };
void BindUFunction(UObject継承クラスのポインタ, メンバ関数のポインタ)
バインドできるのは UFUNCTION マクロを指定した関数のみです。
たいして手間は変わりませんが、BindDynamic マクロを使うこともできます。
マクロの使用例
UCLASS() class ASample : public AActor { GENERATED_BODY() DECLARE_DYNAMIC_DELEGATE(FDelegate); FDelegate Delegate; UFUNCTION() void Test() { UE_LOG(LogTemp, Log, TEXT("%hs"), __FUNCTION__); } public: ASample() { Delegate.BindDynamic(this, &ASample::Test); } virtual void BeginPlay() override { Super::BeginPlay(); Delegate.ExecuteIfBound(); } };
※BindUFunction の使用例は ExecuteIfBound() のサンプルコードをご確認ください。
bool IsBound()
バインドしているオブジェクトがまだ有効なら true。
オブジェクトが無効になっているか、バインドしていないなら false。
bool IsBoundToObject(UObject継承クラスのポインタ)
指定したオブジェクトの UFUNCTION をバインドしているなら true。
指定したオブジェクトが無効、あるいは、このデリゲートがバインドしているオブジェクトが指定したオブジェクトと違うなら false。
bool IsBoundToObjectEvenIfUnreachable(UObject継承クラスのポインタ)
IsBound() と違い、バインドしているオブジェクトが無効になっていても、バインドをしたことがあれば true を返す。
bool IsCompactable()
/** * Checks to see if the user object bound to this delegate will ever be valid again * * @return True if the object is still valid and it's safe to execute the function call */
DeepL 翻訳
このデリゲートにバインドされているユーザーオブジェクトが、今後も有効かどうかをチェックします。
オブジェクトがまだ有効で、関数呼び出しを実行しても安全な場合は真。
…とコメントには書いてあるのですが、実際は逆です。
return FunctionName == NAME_None || !Object.Get(true);
どう見ても、無効な場合に true を返すんですけど?
関数名も意味不明ですし、怪しさ満天なので使わない方が良さそう。
void Unbind()
バインドを解除する。
Clear() も全く同じ。
UObject* GetUObject()
バインドしているオブジェクトを返す。
弱参照が欲しい場合は GetUObjectRef() を使う。
FName GetFunctionName()
バインドしている UFUNCTION の関数名を返す。
Execute()
バインドしているかどうか、安全かどうかに関わらず実行する。
バインドに問題がある場合 check にひっかかるかクラッシュする。
ExecuteIfBound()
バインドしていれば実行する。
バインドしていても、バインドしたオブジェクトが無効になっている場合はクラッシュする。
使用例
UCLASS() class ASample : public AActor { GENERATED_BODY() DECLARE_DYNAMIC_DELEGATE(FDelegate); FDelegate Delegate; UFUNCTION() void Test() { UE_LOG(LogTemp, Log, TEXT("%hs"), __FUNCTION__); } public: ASample() { Delegate.BindUFunction(this, GET_FUNCTION_NAME_CHECKED(ASample, Test)); } virtual void BeginPlay() override { Super::BeginPlay(); Delegate.ExecuteIfBound(); } };
マルチキャストデリゲートは実行リスト(InvokationList)に追加した関数をまとめて呼び出すことができますが、この実行リストに追加する方法は以下の手順になります(引き続きマクロも使えます)。
1. FScriptDelegate に関数をバインドする。
2. 1. をデリゲートに追加する。
UCLASS() class ASample : public AActor { GENERATED_BODY() DECLARE_DYNAMIC_MULTICAST_DELEGATE(FDelegate); FDelegate Delegate; UFUNCTION() void Test() { UE_LOG(LogTemp, Log, TEXT("%hs"), __FUNCTION__); } public: ASample() { FScriptDelegate ScriptDelegate; ScriptDelegate.BindUFunction(this, GET_FUNCTION_NAME_CHECKED(ASample, Test)); Delegate.AddUnique(ScriptDelegate); } virtual void BeginPlay() override { Super::BeginPlay(); Delegate.Broadcast(); Delegate.Clear(); // クリアしないと実行リストに残る } };
UE4 と同じようにマクロを使うこともできます。
UE4 の説明に戻ってご確認ください。
DECLARE_EVENT(イベントを所有するクラス, イベント名)
DECLARE_EVENT_OneParam(イベントを所有するクラス, イベント名,引数1の型)
DECLARE_EVENT_TwoParams(イベントを所有するクラス, イベント名,引数1の型,…)
… NineParams まである
戻り値は指定できません。
DECLARE_DERIVED_EVENT(派生クラス, 親クラス::イベント名, オーバーライドしたイベント名)
マルチキャスト・デリゲートと同じです。
公式の説明
公式のドキュメントから(引用部分以外も含めて)イベント・デリゲートの特徴をまとめると。
- 中身はマルチキャスト・デリゲート
- 実行できるのは宣言したクラスだけ
- 純粋な抽象クラスでコールバックを定義して使う
- 戻り値を持ったデリゲートは宣言できない
- デリゲートを継承するための宣言用マクロがある
ドキュメントによると、宣言したクラスでのみ、デリゲートを実行できる…と書かれているのですが、実際はどこからでも呼べるようになっています。
// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "Sample.generated.h" // FSampleBase だけが FSampleEvent を実行できる(はず) DECLARE_EVENT(FSampleBase, FSampleEvent); class FSampleBase {}; UCLASS() class USample : public UObject { GENERATED_BODY() public: USample() { // バインドはどこでもできる SampleEvent.AddLambda([]() { UE_LOG(LogTemp, Log, TEXT("AddLambda()")); }); } void Invoke() const { // FSampleBase 以外は実行できないはずだが、できてしまう…。 // ランタイムエラーも出ない。 SampleEvent.Broadcast(); } // FSampleBase の外部で使用 FSampleEvent SampleEvent; };
上記のコードは、イベント・デリゲートのルールに従えば間違っているはずですが、ビルドしても実行しても何もエラーは報告されません。
別のテストコード
// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "Sample.generated.h" class IBase { public: DECLARE_EVENT(IBase, FEvent); }; class FOther { public: IBase::FEvent Event; }; UCLASS() class USample : public UObject { GENERATED_BODY() public: USample() { Other = MakeShareable(new FOther()); // バインドはどこでもできる Other->Event.AddLambda([](){ UE_LOG(LogTemp, Log, TEXT("AddLambda")); }); } void Invoke() const { // これも実行可能。 Other->Event.Broadcast(); } private: TSharedPtr<FOther> Other; };
宣言したのとは違うクラスでイベント・デリゲートのインスタンスを持ち、さらにそのインスタンスを別のクラスから呼び出すコードを試してみましたが、こちらも問題なく実行できます。
実際のところ、Broadcast() はどこでも呼び出せるようです。
さらにまた、別のコードを試しました。
// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "Sample.generated.h" class IBase { public: // 存在しないクラスを指定 DECLARE_EVENT(hoge123456789XYZ, FEvent); }; class FOther { public: // 同じく存在しないクラスを指定 DECLARE_DERIVED_EVENT(hoge987654321hoge, IBase::FEvent, FOtherEvent); }; UCLASS() class USample : public UObject { GENERATED_BODY() public: USample() { SampleEvent.AddLambda([](){ UE_LOG(LogTemp, Log, TEXT("AddLambda")); }); } void Invoke() const { SampleEvent.Broadcast(); } FOther::FOtherEvent SampleEvent; };
ビルドもできますし、実行もできます。
宣言用マクロの一番目の引数に指定したクラスが存在しているか?はチェックしていないようです。
不可解な動作をしているので、宣言用マクロを定義している部分のソースコードを調べました。
Engine/Runtime/Core/Public/Delegates/Delegate.h に定義があります。
宣言用マクロは以下の関係になっていることが分かりました。
DECLARE_DERIVED_EVENT(イベントのフレンドクラス, イベントクラスの親クラス, イベントクラス);
↓
class イベントクラス : public イベントクラスの親クラス { friend イベントのフレンドクラス; };
イベントのフレンドクラスは、イベントクラスの protected と private メンバーにアクセスできるようになっています。
宣言用マクロに問題はなさそうです。
問題は DECLARE_EVENT() の方でしょうか…。
// DECLARE_EVENT() の中身 #define FUNC_DECLARE_EVENT( OwningType, EventName, ... ) \ class EventName : public TBaseMulticastDelegate<__VA_ARGS__> \ { \ friend class OwningType; \ };
イベントクラスは、TBaseMulticastDelegate を継承しています。
問題があるのは、このテンプレートクラスのようです。
Broadcast(), IsBound() などのクラスのメンバ関数/変数を外部から呼び出せないようにするには、 protected か private で宣言する必要があります。
現状、外部から呼び出せるようになっているということは、public で宣言されているということになります。
それを確認します。
TBaseMulticastDelegate は Engine/Runtime/Core/Public/Delegates/DelegateSignatureImpl.inl にあります。
// Broadcast() の宣言を抜粋 public: /** * 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
public で宣言してます…。
Broadcast() を外部から呼べなくすることで、意図せず Broadcast() されることを防ぐことができる…というのが、イベント・デリゲートの特徴として紹介されていますが、現状、イベントクラスを外部に出した時点で Broadcast() し放題です。
だからといって、イベントクラスを外部に出さないようにしてしまうと、どこからでもバインドできる…という特徴と矛盾が生じます。
略 // こういう使い方しかできないようにするつもりだった? UCLASS() class USample : public UObject, public FDerived { GENERATED_BODY() public: USample() { this->AddLambda([](){ UE_LOG(LogTemp, Log, TEXT("Inner Derived Lambda()")); }); OuterDerived = MakeShareable(new FDerived()); OuterDerived->AddLambda([](){ UE_LOG(LogTemp, Log, TEXT("Outer Derived Lambda()")); }); // 内部呼び出しなのでOK this->Broadcast(); // 外部からの呼び出しなのでNG OuterDerived->Broadcast(); } TSharedPtr<FDerived> OuterDerived; };
現状、自前でラッパークラスを作るか、エンジンのソースコードを修正するしかないようです。
ラッパークラスは以下のようなものになります。
// クラス設計の例として記載しているだけですので、戻り値と引数は省略しています。 template <typename OwningType> class TEventWrapper { public: AddStatic() AddLambda() AddWeakLambda() AddRaw() AddSP() AddThreadSaveSP() AddUFunction() AddUObject() Remove() Clear() protected: Broadcast() IsBound() IsBoundToObject() private: friend OwningType; イベントクラス MyEvent; };
結構な大きさのテンプレートクラスになりそうです。
作る手間を考えると、マルチキャスト・デリゲートで間に合わせることになりそうです。
変わっていません。
細かい変更はありますが、構造は同じです。
// UE5 での定義 #define DECLARE_EVENT( OwningType, EventName ) FUNC_DECLARE_EVENT( OwningType, EventName, void ) #define FUNC_DECLARE_EVENT( OwningType, EventName, ReturnType, ... ) \ class EventName : public TMulticastDelegate<ReturnType(__VA_ARGS__)> \ { \ friend class OwningType; \ };
TBaseMulticastDelegate から TMulticastDelegate に変わりましたが、使い方は変わりません。
所有クラス (OwningType) は、デリゲート (EventName と 親の TMulticastDelegate) の全てのメンバにアクセスできるので、アクセスするメンバは protected か private でも問題ないです。
ところが、隠ぺいしたいはずの Broadcast() などは全て public のまま変わっていません。
friend class OwningType;
この friend 指定がないとコンパイルが通らないので、その意味では必要です。
ただし、それ以上の設計思想的な意味はないようです。
あるとすれば、イベントとして使用することを示す意味での意味論的な使い道です。
少なくとも、イベントデリゲートは、ただのマルチキャストデリゲートではなく、戻り値がなく、何らかのオブジェクトのイベントとしての使い道を意図したデリゲート…ということをコードを読む人に対して示すことはできます。
使い方を色々と考えてみましたが、完璧に対応するのは C++ の仕組み上難しいですね…。
// 純粋な抽象クラス class ITestEvent { DECLARE_EVENT(ITestEvent, FEvent); virtual ~ITestEvent() {} virtual void OnTestEvent() = 0; // FSample 以外からのアクセスは絶対に許さない friend class FSample; }; // UCLASS は変なマクロで余計なコードが増えるので必要なければ使うのやめます。 class FSample : public ITestEvent { ITestEvent::FEvent Event; // 派生クラス側で public にも protected にもできてしまう virtual void OnTestEvent() override { /* 略 */ } public: FSample() { Event.AddRaw(this, &FSample::OnTestEvent); } void Test() { // 外部から間接的に private メンバにアクセスできる // これを防ぐことはできない Event.Broadcast(); } };
結局のところ、以下の理由で安全なイベントデリゲートを実装するのは無理そうです。
1. 派生クラス側で public や protected などのアクセス指定子を変更できてしまう。
2. 派生クラスの public 関数で呼び出せば外部から間接的にアクセスできてしまう。
結論としては、意味論的な使い道しかないということになります。
イベントデリゲートを使うときは、使い手が使用目的をよく理解し、その目的に合う形で実装する必要があるということになります。
そのせいで、このような無駄な検証を行う手間も増えてしまいます。
何かのクラスの内部だけで使うとか、外部公開しないものなら何も問題ないですが、全てのユーザーに影響する機能として用意する必要があるとは思えないです。
Win32 API で無限に存在していた型名の悪夢を思い出します…。
使い方の説明がどこにもなかったため、ソースコードを読みつつ試行錯誤しました。
※これから説明する内容は、Epic Games に確認をとっておらず、また、誰かと議論したわけでもなく、私ひとりの判断によるものなので、勘違いしている可能性があります。
ソースコードは以下のパスにあります。
Engine/Runtime/CoreUObject/Public/UObject/SparseDelegate.h
Engine/Runtime/CoreUObject/Private/UObject/SparseDelegate.cpp
ソースコードに記載されている説明
1 byte of storage instead of having the full overhead of the delegate invocation list.
The cost to invoke, add, remove, etc. from the delegate is higher than using the delegate
directly and thus the memory savings benefit should be traded off against the frequency
with which you would expect the delegate to be bound.
日本語訳
スパース・デリゲートは頻繁にバインドしないデリゲートに使用することができ、マルチキャスト・デリゲートの実行リスト(invocation list)のように巨大なオーバーヘッドを持つのではなく、1 バイトしか使用しません。
Invoke(), Add*(), Remove*() などのデリゲート用関数を呼び出すコストは、他のデリゲートよりも高いです。
そのため、メモリ節約のメリットはゲリゲートをバインドする頻度とトレード・オフされるべきです。
たまにしか使わない&遅くてもいいから、メモリ節約したい…という用途で使うダイナミック・マルチキャスト・デリゲートです。
一般的に処理速度とメモリ消費はトレードオフの関係にあるので、両方を軽くするのは非常に難しいです。
一般的に消費メモリを節約をしたいなら、処理速度を犠牲にする必要があります。
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE( SparseDelegateClass, OwningClass, DelegateName )
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_OneParam( SparseDelegateClass, OwningClass, DelegateName, Param1Type, Param1Name )
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_TwoParams( SparseDelegateClass, OwningClass, DelegateName, Param1Type, Param1Name, Param2Type, Param2Name )
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_ThreeParams(略)
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_FourParams(略)
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_FiveParams(略)
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_SixParams(略)
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_SevenParams(略)
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_EightParams(略)
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_NineParams(略)
他の宣言用マクロと異なり、引数の型と引数名を指定する必要があります。
値を返すデリゲートは宣言できません(バインドはできそう)。
OwningClass は UObject を継承している必要があります。
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Sample.generated.h" UCLASS() class USample : public UObject { GENERATED_BODY() public: DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_OneParam(FSampleSparseDelegate, USample, SampleSparseDelegate, int, a); USample() { SampleSparseDelegate.AddDynamic(this, &USample::SampleFunction); } void Invoke() { SampleSparseDelegate.Broadcast(123); } UFUNCTION() void SampleFunction(int a) { UE_LOG(LogTemp, Log, TEXT("SampleFunction(%d)"), a); } private: FSampleSparseDelegate SampleSparseDelegate; };
バインド方法が変わりました。
FScriptDelegate を使ってバインドを行います。
この型でバインドできるのは UFUNCTION だけです。
バインドした FScriptDelegate を Add() か AddUnique() に渡すことで、Broadcast() したときにバインドした関数が実行されるようになります。
こちらもダイナミック・デリゲートと同じように、AddDynamic や RemoveDynamic などのマクロを使うこともできます。
使用例
UCLASS() class ASample : public AActor { GENERATED_BODY() DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_OneParam(FSparseTest, ASample, Sparse, int32, a); FSparseTest Sparse; UFUNCTION() void OnSparse1(int32 a) { UE_LOG(LogTemp, Log, TEXT("%hs a=%d"), __FUNCTION__, a); } UFUNCTION() void OnSparse2(int32 a) { UE_LOG(LogTemp, Log, TEXT("%hs a=%d"), __FUNCTION__, a); } void BindOnSparse1() { FScriptDelegate delegate; delegate.BindUFunction(this, GET_FUNCTION_NAME_CHECKED(ASample, OnSparse1)); Sparse.Add(delegate); } void BindOnSparse2() { FScriptDelegate delegate; delegate.BindUFunction(this, GET_FUNCTION_NAME_CHECKED(ASample, OnSparse2)); Sparse.Add(delegate); } public: ASparseTest() { BindOnSparse1(); BindOnSparse2(); } virtual void BeginPlay() override { Super::BeginPlay(); Sparse.Broadcast(123); } };
宣言方法が特殊すぎて使いにくいところは変わっていません。
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE(FSparseTest, ASample, Sparse);
FSparseTest Sparse;