コンテンツにスキップ

ラムダ式・関数ポインタ・std::function

一言で言うと

「関数を値として持ち運ぶ」3つの手段です。

  • 関数ポインタ void(*)(int): 関数のアドレス。状態を持てない。C言語から続く最軽量の形
  • ラムダ式 [x](int y){ return x+y; }: その場で作る無名の関数オブジェクト。変数をキャプチャ(捕獲)できる
  • std::function std::function<void(int)>: 上記すべて(+メンバ関数など)を統一的に入れられる箱。柔軟だがコストあり

C#のdelegateは、この3つの役割を1つでこなす言語機能です(→ 対比の詳細: 第9部)。

関数ポインタ

// C++20
int Add(int a, int b) { return a + b; }
int (*op)(int, int) = &Add;    // 関数のアドレスを変数に
op(2, 3);                      // 5

// 用途: C APIのコールバック、データ駆動のジャンプテーブル

状態(環境)を持てないため、「このボタンが押されたらこの画面を閉じる」のような文脈付き処理には不向きです(C APIはvoid* userData引数で文脈を運ぶ伝統)。

ラムダ式

// C++20
int threshold = 50;

// [キャプチャ](引数){ 本体 }
auto isStrong = [threshold](const Enemy& e) { return e.attack > threshold; };

auto it = std::ranges::find_if(enemies, isStrong);   // アルゴリズムに渡す(最頻出用途)

キャプチャの2形態と寿命の罠

int hp = 100;
auto byValue = [hp]()  { return hp; };     // コピーを保持(安全。以後hpが変わっても影響なし)
auto byRef   = [&hp]() { return hp; };     // 参照を保持(軽いが、hpより長生きしたらdangling!)

// 危険例: ローカル変数を参照キャプチャしたラムダを、後で実行されるキューに積む
// queue.Push([&hp]{ ... });   // この関数を抜けた後に実行されたら未定義動作
// → 後で実行されるラムダは値キャプチャ([=]や[hp])が原則

仕組み: ラムダの正体

ラムダはコンパイラが生成する無名クラスのインスタンスです。

// [threshold](const Enemy& e){ return e.attack > threshold; } はおよそ以下と同じ:
struct __Lambda_1 {
    int threshold;                                  // キャプチャ=メンバ変数
    bool operator()(const Enemy& e) const { return e.attack > threshold; }
};
  • キャプチャなしのラムダは関数ポインタに変換可能
  • テンプレート(find_if等)に渡すとインライン化される——関数ポインタ渡しより速いことが多い(ポインタ経由は最適化しにくい)
  • サイズはキャプチャ量に比例。sizeof可能な普通のオブジェクト

std::function

// C++20
#include <functional>

std::function<void(int)> onDamage;               // 「int を受ける呼び出せる何か」の箱
onDamage = [](int d) { /* ラムダ */ };
onDamage = &FreeFunction;                        // 関数ポインタも
onDamage = std::bind_front(&Hud::Show, &hud);    // メンバ関数+インスタンスも

if (onDamage) onDamage(10);                      // 空チェック→呼び出し

コスト(知っておくべき)

  • 型消去の仕組み上、呼び出しは間接呼び出し(仮想関数相当)。インライン化されない
  • 大きいキャプチャのラムダを入れるとヒープ確保が起きうる(小さければSBO=内部バッファに収まる。閾値は実装依存)
  • → 「コールバックを保存する」用途の標準解だが、ホットループ内での呼び出しの塊には向かない。その場合はテンプレート引数(template <class F> void ForEach(F&& f))で受けるとインライン化される

3手段の使い分け

状況 選択
アルゴリズム・即時実行に渡す ラムダを直接(テンプレート引数で受ける)
コールバックとして保存する(Observer等) std::function(寿命は値キャプチャで守る)
C APIとの境界・データテーブル 関数ポインタ
性能が最優先の内側ループ テンプレート+ラムダ(functionを避ける)

C#との違い

  • C#のラムダのキャプチャは常に参照的(クロージャクラスに変数が持ち上げられ、外の変更が見える)。C++は値/参照を自分で選ぶ——「C#の感覚で書いたら値キャプチャで古い値を見ていた」(またはその逆で参照キャプチャがdangling)が典型事故
  • C#のdelegateはGC管理で寿命の心配がない。C++の参照キャプチャは寿命を自分で保証する
  • +=によるマルチキャスト(複数登録)はdelegate組み込み。C++ではvector<std::function>を自分で持つ(→ Observer)

Unity開発者が誤解しやすい点

  1. 「ラムダを作るとGC Alloc」というUnityの感覚——C++ではキャプチャ付きラムダもスタックに置かれる普通の値。Allocを恐れてラムダを避ける必要はない(std::functionへの格納時のみヒープの可能性)
  2. コルーチンやInvokeの感覚で「後で実行」に参照キャプチャを使うと、C++では即未定義動作
  3. std::functionはdelegateの近似だが、null呼び出しは例外(bad_function_call)、C#のnullデリゲート呼び出しはNullReferenceException——どちらも呼ぶ前チェックの習慣は同じ

ゲーム開発での使用例

  • イベント購読(std::functionのリスト → Observer)
  • 遅延実行キュー(「3秒後にこれ」/フレーム末尾処理 → Event Queue)
  • 軽量Strategy/Command(クラスを作るまでもない差し替え)
  • ソート順・フィルタの指定(std::sort+ラムダ)

使う場面 / 使わない場面

  • ラムダ: 日常のあらゆる場面(C++20の主力構文)
  • std::function: 保存が必要なときだけ(「とりあえずfunctionで受ける」はコストの無駄)
  • 関数ポインタ: C境界・テーブル。新規設計の主役にはしない

よくある誤解

  • 「[&]と書いておけば楽」— 全参照キャプチャは寿命バグの温床。保存されるラムダでは[&]を禁止くらいの規律が安全
  • 「std::functionはラムダの型」— ラムダの型は無名の独自型。functionはそれを入れられる「箱」で、別物

関連項目

理解度チェック

  1. 値キャプチャと参照キャプチャの使い分けの原則(「後で実行」との関係)は?
  2. std::functionのコストを2つ挙げてください。
  3. ラムダの「正体」は何ですか。

演習

samples/lambdas_functions.cpp(検証済み)で、(a) 参照キャプチャのラムダをスコープ外で呼ぶとどうなるか(コメントアウトされた危険例を読む)、(b) std::functionのリストによる簡易イベントを動かしてください。


前: テンプレートと concepts | カテゴリ目次 | 次: enum・enum class・namespace