[C++] C++でC#っぽいdelegateを実装してみる(任意クラスの任意メソッド実行)

昨日に引き続いてC++によるC#のようなDelegateを扱うメモです。
boost.signals2を使えばDelegateクラスをわざわざ実装せずともDelegateのような扱いができることを書きました。
しかし、C#のような任意クラスの任意メソッドを指定することはできませんでした。

結局は関数しか扱えないのか・・・

実はboost.bindを使えば任意クラスの任意メソッドをsignalに指定して実行できるのです。
以下にその使用例を示します。

#include <iostream>
#include <string>
#include <boost/signals2.hpp>
#include <boost/bind.hpp>

using namespace std;
using namespace boost::signals2;

//-------------------------------------
// テストクラス
//-------------------------------------
class A {
public:
    string MethodA(int arg1, float arg2) {
        cout << "A::MethodA(" << arg1 << ", " << arg2 << ") is called." << endl;
        return "return value is a.";
    }
};

class B {
public:
    string MethodB(int arg1, float arg2) {
        cout << "B::MethodB(" << arg1 << ", " << arg2 << ") is called." << endl;
        return "return value is b.";
    }
};

class C {
public:
    string MethodC(int arg1, float arg2) {
        cout << "C::MethodC(" << arg1 << ", " << arg2 << ") is called." << endl;
        return "return value is c.";
    }
};


//-------------------------------------
// メイン関数
//-------------------------------------
int main() {
    boost::signals2::signal<string (int, float)> signal;

    A objA;
    B objB;
    C objC;

    signal.connect(boost::bind(&amp;A::MethodA, &amp;objA, _1, _2));
    signal.connect(boost::bind(&amp;B::MethodB, &amp;objB, _1, _2));
    signal.connect(boost::bind(&amp;C::MethodC, &amp;objC, _1, _2));
    signal.connect(boost::bind(&amp;B::MethodB, &amp;objB, _1, _2));
    signal.connect(boost::bind(&amp;A::MethodA, &amp;objA, _1, _2));
    cout << *signal(123, 45.6) << endl;

    cout << "--------" << endl;

    signal.disconnect(boost::bind(&amp;B::MethodB, &amp;objB, _1, _2));
    signal.connect(boost::bind(&amp;C::MethodC, &amp;objC, _1, _2));
    cout << *signal(456, 7.89) << endl;

    return 0;
}

■実行結果

A::MethodA(123, 45.6) is called.
B::MethodB(123, 45.6) is called.
C::MethodC(123, 45.6) is called.
B::MethodB(123, 45.6) is called.
A::MethodA(123, 45.6) is called.
return value is a.
--------
A::MethodA(456, 7.89) is called.
C::MethodC(456, 7.89) is called.
A::MethodA(456, 7.89) is called.
C::MethodC(456, 7.89) is called.
return value is c.

今回の肝となる部分は以下のboost.bindを使った部分です。

    signal.connect(boost::bind(&amp;A::MethodA, &amp;objA, _1, _2));

通常、関数ポインタにメソッドのポインタを代入することはできません。
また、staticでないメソッドを呼び出すには呼び出し元のインスタンスが存在している必要があります。
しかし、signal.connect()に指定するのは関数ポインタです。

この問題はboost.bindを用いることで解決できます。
boost.bindを用いると、関数の呼び出し時に引数の一部を固定化できるようになります。
たとえば、func(a, b, c)の呼び出しがあるとして、引数aのみを固定させてfunc(b, c)と呼び出すことができます。

C++のメソッド呼び出しは、呼び出し元のインスタンスがメソッドに指定する第1引数の前に隠れて存在しています。
すなわち、obj.method(a, b, c)呼び出しはmethod(obj, a, b, c)と同じ意味になるということです。
このメソッド呼び出しのobj引数を固定化できればmethod(a, b, c)として呼び出すことができ、関数ポインタとして代入できるようになります。

boost::bind()の第1引数にメソッドのポインタ、第2引数に呼び出し元インスタンスのポインタを指定します。
第3引数以降には登録した関数の実行時に指定する引数のフォーマットを指定します。
_1, _2, _3 … というふうにアンダーバー+数字というシンボルをかならず_1から順に個数分だけ指定します。
上のサンプルソースでは、int, floatと引数が2つあるため、_1, _2を指定しています。

固定化できる引数の型は任意であるため、まさに今回の目的には打って付けの道具ですね。
唯一のデメリットはコンパイルエラーが発生したときに意味不明なエラーが出てしまうところです。
何か自作クラスでラップして使えれば問題は回避できそうです。

■参考サイト
シグナル/スロット
【C++ / Boost】boost::bindについての適当まとめ