使うその場で書く関数
前のページでは、オーバーロードによって複数の関数が同じ名前を共有できることを見ました。しかし、名前付き関数がまったく要らないこともあります。ほんの小さなロジックを使うその場で 一度だけ 必要とするとき、それに名前を付けるのは散らかるだけです。それこそがラムダ式です。インラインで定義できる無名関数です。
ラムダ式には特徴的な4つの部分からなる形があります:
[capture](parameters) -> return_type { body }
[] は、いま見ているのがラムダ式だという目印です。戻り値の型は省略可能で、通常はコンパイラが推論します。これが可能な限り最も単純なものです:
greet は単なる変数(型は名前で書き表せないので auto に格納します)で、() で呼び出せます。引数を持つラムダ式は、通常の関数とまったく同じように動きます:
キャプチャ:周囲のスコープに手を伸ばす
ラムダ式を単なる名前なし関数以上のものにしているのがキャプチャリスト、つまり [] です。これによってラムダ式は、自分の引数だけでなく、定義されたスコープの変数も使えるようになります。
[x] で値によるキャプチャ:ラムダ式は自分専用のコピーを得て、それはラムダ式が作られた瞬間に固定されます。
scale(5) が 50 を出力したことに注目してください。ラムダ式が作られたときに存在した factor の値 10 を使っています。値によるキャプチャはスナップショットを取ります。
[&x] で参照によるキャプチャ:ラムダ式は元の変数を参照し、後の変更を見て、変更することもできます。
ラムダ式が使うものすべてを [=](すべて値で)または [&](すべて参照で)でキャプチャすることもできます。便利ですが、明示的にすること([total] や [&total])で、ラムダ式が何に触れるのかが正確に文書化され、考えやすくなります。
ダングリング参照の罠
参照によるキャプチャは強力であると同時に危険でもあります。参照は元の変数が生きている間だけ有効です。ラムダ式がキャプチャした対象より長く生き残ると、ダングリング参照と未定義動作になります。プログラムはクラッシュするかもしれないし、ゴミを出力するかもしれないし、偶然動いているように見えるかもしれません。
これが典型的な間違いです:ローカル変数を参照でキャプチャするラムダ式を返すこと。
auto makeCounter() {
int count = 0;
return [&count]() { return ++count; }; // バグ:count はここで消滅する
}
// 返されたラムダ式は、破棄されたメモリを参照している。
makeCounter が戻るとき、ローカル変数 count は破棄されますが、ラムダ式はまだそれへの参照を保持しています。返されたラムダ式を呼び出すと、死んだメモリに触れることになります。修正は、ラムダ式が自分の状態を所有するように、値でキャプチャすることです:
経験則:参照によるキャプチャは、ラムダ式がすぐにローカルで使われるとき(下のアルゴリズムのように)だけにしましょう。ラムダ式が保存されたり、返されたり、後で実行されたりする瞬間からは、値によるキャプチャを選びましょう。
mutable と戻り値の型
さっきの例の mutable に気づきましたか?デフォルトでは、値によるキャプチャはラムダ式の中で const です。コピーを読むことはできても、変更はできません。mutable を付けると、ラムダ式は呼び出しの間で自分のコピーを変更できるようになります。
mutable はラムダ式のプライベートなコピーにしか影響しません。外側の seen は手つかずのままで、それこそが値によるキャプチャの狙いそのものです。
ほとんどの場合、コンパイラは戻り値の型を問題なく推論します。-> で明示的に書く必要があるのは、分岐によって異なる型を返しうるラムダ式のように、あいまいさがあるときだけです:
// -> がないと、コンパイラは int と double のどちらか決められない
auto half = [](int n) -> double {
if (n % 2 == 0) return n / 2; // int
return n / 2.0; // double
};
ラムダ式とアルゴリズム:本当の恩恵
ラムダ式がC++に追加された理由は、標準ライブラリのアルゴリズムに短いロジックを渡すためです。ラムダ式が登場する前は、別の名前付き関数や、使う場所から離れた扱いにくい関数オブジェクトを書く必要がありました。今ではロジックが呼び出し箇所のすぐそばに置けます。
最もよくある例はカスタムの並べ替え順です:
ここでキャプチャが真価を発揮します。ラムダ式は、フィルタや集計の基準となる値を取り込めるからです。これは、ユーザーが選んだしきい値をいくつの数が超えているかを数えます:
これらのラムダ式はすぐに使われ、周囲の関数より長く生き残らないので、参照でキャプチャしても([&passMark])ここでは安全です。とはいえ、値によるキャプチャも同じくらい明快で、決してダングリングしません。
次は:ポインタ
ラムダ式は、より深い問いをそっと投げかけました。[&x] をキャプチャするとき、ラムダ式は x の 場所 をつかんでおり、その場所は x が生きている間だけ有効です。この考え方、つまり、何かがメモリのどこに存在するかを指し示す値と、指している対象が消えたときに何が起こるか、というのがまさに次のページのテーマです。ポインタに真正面から向き合います。アドレスの取り方、たどり方、そして、いま見たのと同じダングリングの問題がC++全体にどう現れるかを学びます。
よくある質問
C++のラムダ式とは何ですか?
ラムダ式とは、使うその場でインラインに書ける無名関数のことです。構文は [captures](parameters){ body } です。std::sort に渡す比較処理のような、短くて一度きりの処理にぴったりで、別の場所に名前付き関数を宣言する必要がありません。
C++のラムダ式で、値によるキャプチャと参照によるキャプチャの違いは何ですか?
[x] はラムダ式が作られた瞬間に固定された x のコピーをキャプチャします。[&x] は元の x への参照をキャプチャするので、ラムダ式は後の変更を見ることができ、変更することもできます。[&] は、キャプチャした変数がラムダ式より長く生存することが保証されている間だけ使ってください。さもないとダングリング参照になります。
C++のラムダ式で、キャプチャした変数を変更できないと言われるのはなぜですか?
値によるキャプチャは、デフォルトでラムダ式の中では const になります。mutable キーワードを付けると([x]() mutable { x++; })、ラムダ式が自分のコピーを変更できるようになります。ただし、これはラムダ式のコピーを変更するだけで、外側の元の変数は変わりません。