実践C++応用講座こんにちは。ラムダ式は「ある種のクラス(operator()を持っている)」のオブジェクトです。仕組み上ラムダ式はメンバ変数を持つことができます。C++のラムダ式はそれを実にさりげなく使えるようになっています。あまりにさりげないので仕組みが判りにくいです。そこで今回は、その仕組みに焦点を当てて解説しようと思います。1.キャプチャラムダ式は関数内で定義できる「関数のようなもの」です。ローカルに関数を定義できるなら、その定義元のローカル変数にアクセスしたいです。次のようなイメージです。(実際にはC++はローカル関数を定義できませんのでエラーになります。びっくりgccのCコンパイラはローカル関数を定義できました。clangの方はダメでした。foo();}これを可能にする仕組みがラムダ式には備わっています。コピー・キャプチャと参照キャプチャの2種類です。1-1.コピー・キャプチャさて、ラムダ式は関数の中で定義しますが、その寿命は定義した関数の寿命に縛られません。定義した関数の戻り値で返却したり、スレッドを起動してそのスレッド内で呼び出したりした時は、そのラムダ式を定義した関数からreturnした後も、ラムダ式は生きています。これらのケースでは、定義元の関数からreturnし、そのローカル変数が破棄された後でもラムダ式でその値にアクセスしたいことが度々あります。コピーするコードを書けばもちろんできますが、その手間を削減する仕組みとしてコピー・キャプチャがあります。考え方は、関数オブジェクトとしてのラムダ式の内部にメンバ変数を定義し、ラムダ式生成時にそこへコピーしておくというものです。コピーしてしまえば定義元の関数が終了して、ローカル変数が破棄された後もアクセスできます。そのメンバ変数の定義とそこへのコピーをコンパイラが自動的にこっそりとやってくれます。今回の講座では、このコピー・キャプチャについて詳しく解説します。1-2.参照キャプチャまた、キャプチャにはもう一つ参照キャプチャがあります。定義元の関数よりもラムダ式の方が寿命が短いケースも往々にしてありますね。std::sort()の比較関数compにラムダ式を渡すようなケースです。この場合、呼び出したstd::sort関数の中だけでラムダ式を使いますから、定義元の関数が生きている間のみラムダ式が呼ばれます。この時にローカル変数をコピーするのは勿体無いです。このような場合に参照キャプチャが良く用いられます。基本的な考え方はコピー・キャプチャと同じです。ラムダ式の内部に、アクセスしているローカル変数に対応する参照型メンバ変数が自動的にこっそり定義され、自動的にこっそりと初期化されます。定義元の関数が終了するとローカル変数は破棄されますから、定義元の関数終了後に、それらの参照を介して元のローカル変数にアクセスしては行けません。従って、参照キャプチャは定義元の関数よりラムダ式の寿命の方が短い時に使うのが安心です。参照キャプチャについては次回の講座で解説します。2.ローカル変数をコピー・キャプチャするまずは、冒頭で示した不正な処理をラムダ式のコピー・キャプチャしてみます。string>このポイントは10行目です。より、コピー・キャプチャが指定されています。これは「ラムダ式内でアクセスしている親関数のローカル変数」と同じ型同じ名前のメンバ変数を自動的に定義し、そこへコピーしてキャプチャするという意味です。ラムダ式の仕組みはoperator()を持つクラスのオブジェクト(関数オブジェクト)でした。上記サンプルを同等な関数オブジェクトで書いてみます。string>4行目のようにメンバ変数が定義されます。試しにオブジェクトの大きさを比較してみましょう。メンバ変数としてstd::string型を1つ持っているだけですので、std::string型のaStringとラムダ式lambdaと関数オブジェクトaFooのサイズは同じ筈です。=32確かにその通りになりました。Wandboxで確認する。コピー・キャプチャした変数は元の変数ではありませんので、もしも、キャプチャした変数を書き換えても元の変数は書き換えられません。見、この事実は判りにくいです。修飾されているのだと思います。mutableはまた後日解説します。実引数とローカル変数の保存場所スタックで解説しましたが、関数の引数はローカル変数と同様スタック上に記録され、ローカル変数に良く似た振る舞いをします。(ラムダ式の定義元の関数がreturnすると破棄されます。)そして、ラムダ式は関数の引数もローカル変数と同様にキャプチャできます。=40Wandboxで確認する。4.メンバ変数はキャプチャされず、thisがキャプチャされる4-1.メンバ変数がキャプチャされているように見えてキャプチャされていないことメンバ関数内で定義したラムダ式にはメンバ変数もコピー・キャプチャして欲しいものです。そのメンバ関数を呼び出した時のオブジェクトが無くなった後もラムダ式が生きている可能性がありますから。技術的に難しかったのか、実際にはキャプチャされません。キャプチャされるように見えてしまいます。(オブジェクトよりラムダ式の方が寿命が短い時は問題ありません。ラムダ式の寿命の方が長いケースはレアなため利便性を取ったということかも知れません。)次のサンプルを見て下さい。getPrint()でラムダ式を返却しています。この中でmDataにアクセスしています。`[=]’でコピー・キャプチャを指定しているので、mDataがコピーされていることを期待しますね。aPrint();}しかし、(Wandboxでは)次のような結果になりました。mDataのアドレスに着目して下さい。fooのコンストラクタで表示したアドレスと同じアドレスがaPrint()を実行した時にも表示されています。ラムダ式の内部メンバ変数へコピーされていないということです。&mData=0x7ffe085cb210では何故メンバ変数にアクセスできるのかといいますと、メンバ関数には隠しパラメータとしてthisが渡っています。このthisがこっそりキャプチャされているのです。その証拠にint型が4バイト、ポインタ型が8バイトの環境(Wandboxはこれに該当します)を使って、aPrintのサイズを見るとmDataのサイズである4バイトではなくthisポインタのサイズである8バイトとなります。getPrint2()メンバ関数で、ローカル変数aDataへmDataをコピーしてから、aDataだけをラムダ式でアクセスするようにしたaPrintaのサイズを確認すると4バイトになります。こちらはthisポインタではなく、4バイトのaDataだけがキャプチャされたということです。=8sizeof(aPrint2)=4Wandboxで確認する。4-2.このことを知らないと「やらかしてしまう」かもしれないこと4-1の事実を把握していない場合、ラムダ式をメンバ関数で返却する際にメンバ変数をコピー・キャプチャしたつもりでコピー・キャプチャできていないまま返却するかも知れません。(私も機会があったら既にやらかしていたかもしれません。やらかす前にここを読んだので済んでいるだけです。aPrint();}getFooPrint()関数でfooクラスのオブジェクトを作り、そのgetPrint()を呼び出して、メンバ変数mDataをコピー・キャプチャした(つもり)のラムダ式を返却しています。本当にコピー・キャプチャであれば問題ないコードです。getFooPrint()関数内でローカル変数として作ったfooオブジェクトのメンバ変数のままです。getFooPrint()関数からreturnしたあとは、ラムダ式が保持するthisの先のfooオブジェクトはデストラクトされています。(このタイミングでデストラクタが呼ばれます。何が入っているか分かりません。dummy[10]配列のサイズを変えたり、コンパイラのバージョンを変えたりすると結果が変わってしまいます。std::coutのoperator<<()呼び出しでスタック上のデータが上書きされますが、dummyのサイズを変えることで、mDataがスタック上のどこに記録されるのかが変わり、その結果operator<<()による上書きの影響が変わるからです。&mData=0x7fff8b576b80Wandboxで確認する。2017でやってみたところ、嫌な結果でした。デストラクタで設定しなければ元の値をアクセスできました。なると、このバグに気がつくのは至難の業です。C++では、fooオブジェクトがデストラクトされているにも関わらずfooオブジェクトに割り当てられていたメモリが開放されていないようです。最後に設定した値が残っているということのようです。これは保証された動作ではありません。バグが顕在化しにくくなっただけです。&mData=000000E2FD8FFD785.グローバル変数とstaticなローカル/メンバ変数はキャプチャしないグローバル変数はキャプチャするまでもなく関数終了後も有効ですし、更に特殊なことをしなくても普通にアクセスできますのでキャプチャされません。グローバル変数と同様に静的領域に記録されるstaticなローカル変数はどうなるのでしょう?結論から言うと、アクセスはできますがグローバル変数同様キャプチャされない(メンバ変数が定義されない)ようです。実は通常のグローバル変数とstaticなローカル変数の違いはアクセス制限だけです。クラスのメンバのprivateやpublicと同様なのです。これらはバグを発生しにくくするためにアクセスさせたくない領域へのアクセスを制限するものです。アクセスしていたらコンパイラが検出してエラーにするという仕組みです。関数内で定義されたラムダ式に対してstaticなローカル変数をアクセスできないように制限したいケースはまずないと思います。アクセス制限していないというだけですね。同じことを関数オブジェクトで行う術(構文)はないので、ラムダ式ならではの機能です。2.のサンプルのaStringをstaticにした時の実行結果です。=32ラムダ式はメンバ変数が1つも自動定義されないのでデフォルトのサイズ1となっています。関数オブジェクトfooは明示的にメンバ変数を定義してますので元のサイズと変わりません。staticなメンバ変数も実験してみましたが、staticなローカル変数と同様でした。意味的にはstaticなローカル変数とほぼ同じですから当然ですね。Wandboxで確認する。6.まとめ今回は、ラムダ式のコピー・キャプチャについて詳しく解説してみました。ラムダ式を定義している関数よりラムダ式の方が寿命が長い時は原則としてコピー・キャプチャを使いましょう。キャプチャできるものはローカル変数と関数の引数だけです。メンバ変数は一見キャプチャされているようにみえてコピー・キャプチャされていません。5.で解説したようになかなか危険ですのでメンバ変数を誤ってコピー・キャプチャしたつもりにならないよう要注意です。次回はたどり着けなかった参照キャプチャを解説します。コメントをキャンセルメールアドレスが公開されることはありません。テクノロジーの登録商標です。technology.。