【C++】std::functionとは?|関数やラムダを変数に入れて渡す仕組み

C++のstd::functionのイメージ|関数やラムダを変数に入れて渡す C++

今回は、C++でよく出てくる「std::functionとは何か」について解説していきます。

「std::functionって何?」
「関数ポインタと何が違うの?」
「ラムダやメンバ関数もまとめて扱えるって聞いたけど、どういうこと?」

こんな疑問はありませんか?

std::function は、ざっくり言うと「呼び出せるもの(関数・ラムダ・関数オブジェクト)を、型を気にせず1つの変数に入れておける入れ物」です。ボタンが押されたときの処理や、敵が死んだときのコールバックを「あとから差し替えられる」ようにするのに大活躍します。

この記事を読み終えると、あなたはstd::functionの役割・書き方・関数ポインタとの違い・ゲームでの使いどころ・ハマりやすいミスをしっかり理解できると思いますので、ぜひ最後まで読んでいただけると嬉しいです。

std::functionの正体

std::function<functional> にある標準ライブラリの型です。「どんな引数を受け取って、何を返すか」だけを決めておけば、中身は自由に差し替えられるのが特徴です。

#include <functional>
#include <iostream>

// int を2つ受け取って int を返す「入れ物」
std::function<int(int, int)> op;

int Add(int a, int b) { return a + b; }

int main() {
    op = Add;                       // 普通の関数を入れる
    std::cout << op(2, 3) << "n";   // 5

    op = [](int a, int b) {         // ラムダを入れる
        return a * b;
    };
    std::cout << op(2, 3) << "n";   // 6
    return 0;
}

std::function<int(int, int)>int(int, int) の部分が「型(シグネチャ)」です。同じ入れ物に、普通の関数もラムダも入れられるのが分かりますね。

関数ポインタとの違い

「それ、関数ポインタでもできるのでは?」と思うかもしれません。大きな違いは「状態を持ったラムダ(キャプチャ付き)」も入れられることです。

  • 関数ポインタ … 素の関数しか入らない。キャプチャありラムダは不可
  • std::function … 関数・キャプチャありラムダ・関数オブジェクトを何でも入れられる

そのぶん、std::function は内部で少しだけ余分な処理をしています。柔軟さと引き換えに、生の関数ポインタよりわずかに遅い、という関係です。

ゲームでの使いどころ|コールバックを差し替える

一番おいしい使い方がコールバックです。UIボタンに「押されたら何をするか」を外から渡す、という形にできます。

class Button {
public:
    // 「押されたときの処理」を外から受け取る
    void SetOnClick(std::function<void()> callback) {
        onClick_ = callback;
    }
    void Press() {
        if (onClick_) onClick_(); // 入っていれば呼ぶ
    }
private:
    std::function<void()> onClick_;
};

// 使う側
Button start;
int score = 0;
start.SetOnClick([&]() {
    score += 100;            // キャプチャした score を触れる
});
start.Press();               // score が 100 になる

Button クラスは「何をするか」を一切知りません。処理を丸ごと外から差し込めるので、ボタンごとに違う動きを付けられます。似た考え方のオブザーバーパターンと組み合わせると、さらに疎結合な設計になります。

【重要】私がstd::functionで固まった体験談

コールバックにラムダで [&](参照キャプチャ)を使い、その場の一時オブジェクトを参照していたら、呼び出した瞬間にクラッシュしました。

原因はダングリング参照。ラムダを登録したあとに、参照していた変数が寿命を迎えて消えていたんです。std::function は中身を保持し続けるので、「いつ呼ばれるか分からないコールバックで [&] を使うと危ない」と痛感しました。長生きするコールバックでは、値キャプチャ [=] か、必要な値だけをコピーで渡すのが安全です。

ここを理解してからは、コールバックの寿命を意識するようになり、原因不明のクラッシュがぐっと減りました。

std::functionのよくある失敗例と対処法

①空のまま呼び出してクラッシュ

何も入れていない std::function を呼ぶと std::bad_function_call で落ちます。呼ぶ前に if (f) で中身があるか確認しましょう。

②[&]キャプチャでダングリング参照

体験談のとおりです。すぐ呼ぶなら [&] でOKですが、あとで呼ばれるコールバックは値キャプチャを基本にすると安全です。

③毎フレーム大量に作って重い

std::function は生成・コピーに多少コストがあります。更新ループの中で毎回作り直さず、一度作って使い回すのが定石です。ホットな場所では関数ポインタやテンプレートも検討します。

注意点

  • 呼ぶ前にif (f) で空チェックする
  • 長生きするコールバックで[&] を使わない(寿命に注意)
  • 速度が命の場所では関数ポインタやテンプレートも選択肢に

まとめ

  • std::functionは呼び出せるものを入れておける入れ物
  • 関数ポインタと違いキャプチャありラムダも入る
  • ゲームではコールバックの差し替えで活躍する
  • ハマりどころは空呼び出しと参照キャプチャの寿命

std::function を使えると、処理を「データのように渡す」設計ができるようになります。まずはボタンのコールバックから試してみてください。

関連記事