C++でデリゲートを実装
はじめに
UnrealC++
のデリゲート機能TDelegate
の簡易版を自作してみようというもの
C#
unityでシーン読み込みのC#スクリプトを書いている時にデリゲートって便利だなと思いました。SceneManager
はunity君がやってるものですけど、デリゲートに関してはC#
の言語仕様だからすごいですよね。カンタン!キレイ!
// シーン読み込み完了時に呼ばれる
SceneManager.sceneLoaded += OnSceneLoaded;
C++
C++にはデリゲートという言語仕様はなく、関数ポインターを代理クラスに渡して処理してもらう形式(コールバック関数)を利用していると思います。これといって困ったことはないですが、UE5
に用意されているデリゲート機能が便利だったので、UE風なデリゲートを自作してみようと思いました。これは、UE5でレベル読み込み開始時にMoviePlayer
クラスが行う処理です。
// レベル読み込み開始時に呼ばれる
FCoreUObjectDelegates::PreLoadMap.AddRaw(this, &FDefaultGameMoviePlayer::OnPreLoad);
目指す要件
- メンバー関数 / 静的メンバ関数 / ラムダ / 通常関数 のすべてが1つのクラスで扱える
- UE5 と同じフォーマットで記述できる
1 に関しては、実際には4つのパターンがあるのではなく、すべての関数を1つのインターフェースで扱うことができるようにする。ということです。関数の種類ごとにクラスや取り扱い分かれているのはわかりずらいと思います。
2 が個人的に重要視していたポイントで、UE5と同じフォーマットで記述できるということです。必要なのは、関数を呼ぶインスタンスと、呼ぶメンバ関数ポインタだけなので、それ以上のパラメータを記述しない様にしたいです。
m_OnSceneLoadEvent.Bind(this, &Hoge::OnSceneLoad);
std::function
でメンバ関数をバインドする場合はstd::bind
を使うのですが、インスタンスポインタ―・関数ポインタに加えて、引数の数だけstd::placeholders::_N
を記述する必要があります。このstd::placeholders::_N
を取り除くことを目標にします。
std::function<void(float)> fn;
void Object::Update(float deltaTime){}
fn = std::bind(&Object::Update, this, std::placeholders::_1);
fn(1.0f);
UE5 TDelegate
UEには、DECLARE_DELEGATE
マクロが用意されていて、テンプレート引数も省略されています。DECLARE_DELEGATE
マクロは、指定した関数シグネチャを持つデリゲート型の型エイリアスを定義します。以下のコードでは、TDelegate<void()>
の型エイリアスとなるOnInit
を定義しています。DECLARE_DELEGATE
マクロ以外にも、DECLARE_DELEGATE_OneParam
のような、引数の数に応じたマクロが用意されており、最大9個の引数をとるDECLARE_DELEGATE_NineParams
まで定義されています。
DECLARE_DELEGATE(OnInit) // typedef TDelegate<void()> OnInit;
OnInit OnInitDelegate;
OnInitDelegate.BindRaw(this, &Hoge::OnInit);
OnInitDelegate.Excute();
UE5 TMulticastDelegate
今回自作したものはシングルキャストデリゲート(1つのデリゲートインスタンスに対して1つのリスナーという1対1の関係)ですが、デリゲートといえばマルチキャストデリゲートが一般的で、1つのデリゲートに対して複数のインスタンスがバインド可能(1対多)です。実行時にはバインドされているすべてのインスタンスに通知されます。
struct COREUOBJECT_API FCoreUObjectDelegates
{
// void(const FString&)型の マルチキャストデリゲート
DECLARE_MULTICAST_DELEGATE_OneParam(FPreLoadMapDelegate, const FString&);
static FPreLoadMapDelegate PreLoadMap;
...
}
// バインド
FCoreUObjectDelegates::PreLoadMap.AddRaw(this, &FDefaultGameMoviePlayer::OnPreLoad);
// すべてのリスナーに対してコールバック
FCoreUObjectDelegates::PreLoadMap.Broadcast("SceneName");
実装
UE5のTDelegate
を意識して実装したので、使い方はほぼ同じです。異なる点は、Bind
メンバ関数です。UE5には、ラムダ用、スマポ用、スレッドセーフ用、生ポインタ用など、さまざまな関数がありますが、自作Delegate
には、非静的メンバー関数 / その他の関数用でオーバーロードされた2つのBind
メンバ関数だけです。DECLARE_DELEGATE
マクロも同様にありますが、引数ありを定義するためのマクロ群はDECLARE_DELEGATE_9_Params
のように数字に変えてあります。
// メンバー関数用
template<typename Class>
void Bind(Class* instance, ReturnType(Class::* memberFunction)(Args...));
// その他関数(ラムダ・staticメンバ関数・通常関数)
void Bind(FunctionType function);
使用例
int main()
{
Player player;
// メンバ関数
Delegate<float(int, float)> delegate;
delegate.Bind(&player, &Object::MyMemberFunction);
float memberResult = delegate.Execute(42, 3.14f);
// 通常関数
Delegate<void(const char*)> staticDelegate;
staticDelegate.Bind(&Function);
staticDelegate.Execute("Hello World!");
// ラムダ
Delegate<int(int, int)> lambdaDelegate;
lambdaDelegate.Bind([](int a, int b) -> int {
return a - b;
});
int lambdaResult = lambdaDelegate.Execute(4, 3);
}
Discussion