C++11 の std::bind と std::function を活用した cocos2d-x 3.0 のコールバック設計

cocos2d-x 3.0 では、C++11 の新機能を積極的に取り入れており、特に std::bindstd::function を活用したコールバック機構が導入された。これにより、従来のマクロベースのセレクタ方式(例:menu_selector)から、より型安全で柔軟な関数オブジェクトによるイベントハンドリングへと進化している。

たとえば、メニュー項目の作成コードは以下のように記述される:

auto closeItem = MenuItemImage::create(
    "CloseNormal.png",
    "CloseSelected.png",
    CC_CALLBACK_1(HelloWorld::menuCloseCallback, this)
);

ここで使用されている CC_CALLBACK_1 は、内部で std::bind をラップしたマクロであり、その定義は次の通りである:

#define CC_CALLBACK_0(__selector__, __target__, ...) \
    std::bind(&__selector__, __target__, ##__VA_ARGS__)
#define CC_CALLBACK_1(__selector__, __target__, ...) \
    std::bind(&__selector__, __target__, std::placeholders::_1, ##__VA_ARGS__)
#define CC_CALLBACK_2(__selector__, __target__, ...) \
    std::bind(&__selector__, __target__, std::placeholders::_1, std::placeholders::_2, ##__VA_ARGS__)

この数字は、対象メンバ関数が受け取る引数の数を示しており、std::placeholders::_N を使って未束縛のパラメータをプレースホルダとして指定する仕組みになっている。

同様に、タッチイベントリスナーの設定も以下のように記述される:

listener->onTouchesBegan = CC_CALLBACK_2(Layer::onTouchesBegan, this);

このフィールドの型は次のように定義されており、std::function を用いてコールバックのシグネチャを明確に規定している:

std::function<void(const std::vector<Touch*>&, Event*)> onTouchesBegan;

std::bind の基本的な使い方

std::bind は、関数やメンバ関数を部分適用(partial application)し、新たな呼び出し可能なオブジェクトを生成する。例えば、3引数関数を1引数関数に変換する例:

int multiply(int a, int b, int c) {
    return a * b * c;
}

auto partial = std::bind(multiply, 2, std::placeholders::_1, 3);
int result = partial(5); // multiply(2, 5, 3) = 30

メンバ関数をバインドする際には、インスタンス(またはポインタ)を最初の引数として渡す必要がある:

class Calculator {
public:
    int add(int x, int y) { return x + y; }
};

Calculator calc;
auto bound_add = std::bind(&Calculator::add, &calc, std::placeholders::_1, 10);
int val = bound_add(5); // calc.add(5, 10) = 15

std::function の役割

std::function は、任意の呼び出し可能オブジェクト(関数ポインタ、ラムダ式、関数オブジェクト、std::bind の結果など)を統一的に保持・呼び出しできる型安全なラッパーである。

std::function<int(int, int)> op;

// 関数ポインタを代入
op = [](int a, int b) { return a + b; };
int sum = op(3, 4); // 7

// メンバ関数を bind して代入
Calculator calc;
op = std::bind(&Calculator::add, &calc, std::placeholders::_1, std::placeholders::_2);
sum = op(6, 7); // 13

これにより、コールバックやイベントハンドラを柔軟に切り替えられる設計が可能になる。たとえば、条件に応じて異なる処理を注入する関数:

void executeIfEven(int value, std::function<void(int)> callback) {
    if (value % 2 == 0) {
        callback(value);
    }
}

void print(int x) { std::cout << "Value: " << x << std::endl; }
void doublePrint(int x) { std::cout << "Double: " << x * 2 << std::endl; }

// 使用例
auto f1 = std::bind(print, std::placeholders::_1);
executeIfEven(4, f1); // 出力: Value: 4

f1 = std::bind(doublePrint, std::placeholders::_1);
executeIfEven(6, f1); // 出力: Double: 12

注意点

  • std::bind で束縛した値はコピーされる(pass-by-value)。
  • プレースホルダ(_1, _2, ...)は参照渡しではなく、実引数として渡された値が使用される。
  • メンバ関数をバインドする際は、オブジェクトのライフタイムに注意が必要。無効なポインタや参照を保持すると未定義動作を引き起こす。
  • Visual Studio 2012 では、メンバ関数ポインタを直接 std::function に代入できない場合があり、std::mem_fn を経由する必要がある。

cocos2d-x 3.0 におけるこれらの変更は、C++11 の表現力を活かし、よりモダンで安全なゲーム開発を可能にしている。

タグ: C++11 std::bind std::function cocos2d-x

8月12日 03:21 投稿