📩

C++でデリゲートを実装

2023/06/12に公開

はじめに

UnrealC++のデリゲート機能TDelegateの簡易版を自作してみようというもの

C#

unityでシーン読み込みのC#スクリプトを書いている時にデリゲートって便利だなと思いました。SceneManagerはunity君がやってるものですけど、デリゲートに関してはC#の言語仕様だからすごいですよね。カンタン!キレイ!

// シーン読み込み完了時に呼ばれる
SceneManager.sceneLoaded += OnSceneLoaded;

https://docs.unity3d.com/ja/2023.2/ScriptReference/SceneManagement.SceneManager.html

C++

C++にはデリゲートという言語仕様はなく、関数ポインターを代理クラスに渡して処理してもらう形式(コールバック関数)を利用していると思います。これといって困ったことはないですが、UE5に用意されているデリゲート機能が便利だったので、UE風なデリゲートを自作してみようと思いました。これは、UE5でレベル読み込み開始時にMoviePlayerクラスが行う処理です。

// レベル読み込み開始時に呼ばれる
FCoreUObjectDelegates::PreLoadMap.AddRaw(this, &FDefaultGameMoviePlayer::OnPreLoad);

https://docs.unrealengine.com/5.2/ja/delegates-and-lamba-functions-in-unreal-engine/

目指す要件

  1. メンバー関数 / 静的メンバ関数 / ラムダ / 通常関数 のすべてが1つのクラスで扱える
  2. 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