【C++】ラムダ式とは?|関数や処理をその場で作って渡す仕組み

記事のアイキャッチ。C++のラムダ式とは? 関数や処理をその場で作って渡す仕組み C++

[]() {}と書くだけで、名前のない関数が作れます。ラムダ式は、別の場所に関数を定義しなくても、コールバックや、ソートの比較関数を、その場で完結させられる仕組みです。ただし、[=]と[&]のキャプチャの違いを、あいまいに使うと、予想外の動作につながります。

この記事では、基本の書き方、値キャプチャと参照キャプチャの違い、mutable、std::functionとの併用、参照キャプチャの危険性まで、実際にコンパイル・実行した結果を交えて解説します。

ラムダ式の基本の書き方

ラムダ式は次の5つの部分からできています。

[キャプチャ](引数) -> 戻り値の型 { 処理 }

一番シンプルな例はこれです。

auto add = [](int a, int b) { return a + b; };
int result = add(2, 3); // 5

[] の部分がキャプチャです。ここに、ラムダ式の外側にある変数を中で使うかどうか、使うならどう使うかを指定します。このキャプチャこそが、ラムダ式を普通の関数と別物にしている部分です。

【実測】値キャプチャ [=] と参照キャプチャ [&] の違い

キャプチャには主に2種類あります。

  • 値キャプチャ [x]… ラムダ式を作った時点の値をコピーして抱え込む
  • 参照キャプチャ [&x]… 元の変数を参照として見に行くだけ(コピーしない)

言葉だけでは伝わりづらいので、実際に両方を作って、後から元の値を書き換えてから呼び出すことで違いを確かめてみました。

int counter = 0;
auto byValue = [counter]() { printf("byValue: %d\n", counter); };
auto byRef   = [&counter]() { printf("byRef  : %d\n", counter); };

counter = 10; // ラムダ式を作ったあとで値を書き換える

byValue(); // ラムダ式を作った時点の値のまま
byRef();   // 今の値を見に行く

実行結果は記事の後半でまとめて載せていますが、先に結論だけ書くと、byValueは0のまま、byRefは10に変わりました。値キャプチャは「写真を撮る」、参照キャプチャは「実物を見に行く」とイメージすると分かりやすいです。

mutable|値キャプチャした変数を書き換えたいとき

値キャプチャした変数は、初期状態ではラムダ式の中で書き換えられません(const扱い)。書き換えたい場合は mutable を付けます。

int seed = 0;
auto counterLambda = [seed]() mutable {
    seed++;
    return seed;
};
counterLambda(); // 1
counterLambda(); // 2
counterLambda(); // 3
// 呼び出し元のseedはすっと 0 のまま

mutableで変わるのはあくまでラムダ式自身が抱えているコピーだけで、呼び出し元のseedには一切影響しません。回数を数えるカウンターをラムダ式単体で持たせたいときなどに使えます。

std::functionと併用してコールバックとして使う

ラムダ式の代表的な使い道が、std::functionと組み合わせたコールバックです。「ボタンが押されたら何をするか」を、ボタン側では中身を知らずに外から差し込めます。

class Button {
public:
    std::function<void()> OnClick;
    void Press() {
        if (OnClick) OnClick();
        else printf("(OnClick未設定)\n");
    }
};

Button button;
int clickCount = 0;

// この場で処理を作ってそのまま渡す
button.OnClick = [&clickCount]() {
    clickCount++;
    printf("ボタンが押されました(%d回目)\n", clickCount);
};

button.Press(); // ボタンが押されました(1回目)
button.Press(); // ボタンが押されました(2回目)

Buttonクラス自体は「押されたらOnClickを呼ぶ」しか知りません。中身をラムダ式で差し込むだけで、ボタンごとに違う動作をさせられるのがラムダ式×std::functionの強みです。

ジェネリックラムダ(autoparam)

C++14以降、引数の型をautoにすると、型に依存しないラムダ式を作れます。

auto add = [](auto a, auto b) { return a + b; };

add(2, 3);     // intで足し算 -> 5
add(2.5, 1.5); // doubleで足し算 -> 4.0

同じaddという名前をもう一つ別の型用に用意する必要がなく、テンプレート関数を軽く書く感覚で使えます。

実際に全部動かしてみた

ここまでのコードを小さなプログラムにまとめて、Visual Studio(MSVC、C++17)で実際にビルド・実行した結果がこちらです。

[1] 値キャプチャ vs 参照キャプチャ
    byValue: 0
    byRef  : 10

[2] mutable
    1回目: 1
    2回目: 2
    3回目: 3
    呼び出し元のseedは変化なし: 0

[3] コールバック登録
    ボタンが押されました(1回目)
    ボタンが押されました(2回目)
  (OnClickが未設定)

[4] ジェネリックラムダ
    add(2, 3)     = 5
    add(2.5, 1.5) = 4.0

[5] 参照キャプチャの寿命切れ(危険な例)
    safeCounter()     = 100  (正しく100)
    danglingCounter() = 8  (関数を抜けた変数を見ている=未定義動作)

先頭で見た値キャプチャvs参照キャプチャも、mutableも、想定どおりの結果になりました。目を引くのは[5]です。

【重要】記事を書くこのデモで実際に見つけた危険なパターン

この記事用に実際のデモを書いていて、[5]の部分で本当に足をすくった。以下の2つの関数を比べてみます。

std::function<int()> MakeDanglingCounter() {
    int localValue = 100;                       // この関数を抜けたら消える
    return [&localValue]() { return localValue; }; // 参照キャプチャ
}

std::function<int()> MakeSafeCounter() {
    int localValue = 100;
    return [localValue]() { return localValue; };  // 値キャプチャ
}

やっていることはほとんど同じです。違うのは[&localValue](参照)か[localValue](値)かだけ。でも実行結果はsafeCounter()が正しく100を返す一方で、danglingCounter()は8という意味の分からない値を返しました。

原因は単純で、MakeDanglingCounterがreturnした瞬間にlocalValueというローカル変数は寿命を終えます。参照キャプチャしたラムダ式は、もうない変数への参照を持ち続けるので、後で呼び出すと何が返ってくるか保証されません。今回はたまたま8という値が返ってきましたが、環境やビルド設定によってはクラッシュすることもあります。

怖いのは、コンパイルは一切警告を出さないことです。型は合っているし、文法も正しいので、実際に呼んでみるまで問題に気づけません。std::functionでコールバックを作って外部に渡すときは、参照キャプチャしている変数の寿命がコールバックを呼ぶ時点まで持つかを必ず確認するべきだと、自分でデモを作ってめちゃくちゃ実感しました。

ラムダ式のよくある失敗例と対処法

①コールバックに[&]でローカル変数を捕まえてしまう

上で実演した通りです。ボタンのOnClickのようにすぐに呼ばれる前提のコールバックなら[&]で問題ありませんが、あとで何度も呼ばれる可能性があるコールバックは値キャプチャ[=]が基本です。迷ったら「この変数、ラムダ式より長生きする?」を自問しましょう。

②値キャプチャしたのに中身が更新されない

「ローカル変数を変えたのに、ラムダ式内の値が古いまま」という失敗は、値キャプチャは作った時点のコピーだと忘れていることが原因です。最新の値を見たいなら参照キャプチャを選びましょう。

③mutableを付け忘れてコンパイルエラー

値キャプチャした変数をラムダ式内で書き換えようとすると、constな変数を代入しようとしたときと同じエラーになります。mutableを付ければ解決しますが、変数を書き換えたくなった時点で、参照キャプチャでもよかったのでは?と一度立ち止まるのが安全です。

注意点

  • 値キャプチャ[=]…作った時点のコピー。寿命の心配がない
  • 参照キャプチャ[&]…最新の値を見られるが、元の変数より長生きしてはいけない
  • 値キャプチャを書き換えたいならmutable。ただし元の変数は変わらない
  • すぐに使うなら[&]、とっておく/外に渡すなら[=]を基本方針にすると事故が減る

結論

  • ラムダ式は名前のない関数をその場で作る仕組み
  • 値キャプチャ[=]はコピー、参照キャプチャ[&]は実物を見に行く
  • 値を書き換えたいだけならmutable
  • std::functionと併用すれば、中身を外から差し込めるコールバックになる
  • 参照キャプチャしたラムダ式を外に持ち出すときは、寿命に注意(実際に未定義動作を見た通り)

値キャプチャの[=]はコピー、参照キャプチャの[&]は、実物を見に行きます。値を書き換えたいだけならmutableで、std::functionと併用すれば、中身を外から差し込めるコールバックになります。参照キャプチャしたラムダ式を、外へ持ち出すときは、寿命に注意してください。実際に、未定義動作を確認しています。

関連記事