ラムダ式ネタをもう一つ。こっちは少々マニアックな内容です。
int f( std::vector<int> const& v1, std::vector<int> const& v2 ) { int sum = 0; auto add_to_sum = [&](int x){ sum += x; }; std::for_each( v1.begin(), v1.end(), add_to_sum ); std::for_each( v2.begin(), v2.end(), add_to_sum ); return sum; }
のように、ローカル変数を参照キャプチャすることが可能ですが、
一般に、ローカル変数に対する参照には、 dangling reference*1 が生じる可能性、というものが存在します:
// ラムダ式とは関係ない例 std::string& f() { std::string x; return x; // ローカル変数への参照を返している! } void g() { std::string& x = f(); x = "hoge"; // ダメ。 x の参照先は既に破棄されている }
具体的には、関数の戻り値として、ローカル変数への参照を返した場合、
そのローカル変数は関数の終わりに破棄されるため、 dangling reference となります。
しかし、ラムダ式においては、ローカル変数を参照キャプチャした場合でも、基本的には安全です。
何故ならば、ローカル変数に対する参照が dangling reference になる場合は、上記のように
関数の戻り値としてローカル変数への参照を返した場合の他には、基本的にはありえないからです。*2
そして、ラムダ式に対する decltype は行えない以上、関数の戻り値として ローカル変数を参照キャプチャしたラムダ式を使うことは不可能です*3:
auto f( std::string x ) -> decltype( [&](){ return x; } ) // こうは書けない { return [&](){ return x }; }
このような理由により、ローカル変数を参照キャプチャしたラムダ式は、基本的に安全なのです。
ただし、「基本的に」安全だ、と表記したように、安全でない場合も、実は存在します。
具体的には、 type erasure*4 を使うなどして、ラムダ式の持つ「再束縛できない*5」「関数の戻り値として使えない」という性質が破られた場合には、ラムダ式によってキャプチャしたローカル変数への参照は dangling reference となりえます。
type erasure の例としては、 std::function や std::shared_ptr などが挙げられ、
void f() { std::function<std::string ()> func; { std::string x; func = [&](){ return x; }; } // x はここで破棄されるので // この呼び出しは未定義動作となる std::string s = func(); } std::shared_ptr<X> g() { bool deleted = false; auto del = [&]( X* p ){ delete p; deleted = true; } return std::shared_ptr<X>( new X(), del ); // del は deleted を参照しているのでアウト }
上記のようなコードは、いずれも dangling reference にアクセスし、未定義動作を引き起こします。
このように、 std::function や std::shared_ptr といった type erasure は、便利ではあるのですが、
その使用の際には、変数の寿命を考慮した上で、必要ならば値キャプチャを使う等の工夫が必要になります。
このような危険性は、別にラムダ式と type erasure を組み合わせた場合に限らず、
一般に、参照を保持する変数を「再代入する*6」場合と「関数から返す」場合には、
dangling reference が生じる可能性がある、ということは、きちんと把握しておくべきです。
しかし、このことは、逆に言えば、
これらの操作を行わない場合、 C++ の参照は非常に安全に扱うことが出来る、
ということでもあるので、覚えておいて損はない筈です。
追記 (2013/05/26)
2014年に策定予定の C++14 では,関数の戻り値を return 文から推論できるようになったため,
上記のような場合に加えて,関数の戻り値として使う場合も考慮する必要があります.
// C++14 では, auto を使うことで一般関数の戻り値も推論可能 auto f(int x) { return [&] (int y) { return x + y; }; }; auto g = f(1); g(2); // undefined behavior, x は既に破棄されている
また,C++11 *7 でも,ラムダ式の型推論を使えばラムダ式を関数から返せるので,
そのような場合には, dangling reference に気をつける必要があります.
auto f = [] (int x) { return [&] (int y) { return x + y; }; }; auto g = f(1); g(2); // undefined behavior, x は既に破棄されている
総じて,「再代入を始めとしたオブジェクトの状態の変更」と「関数の戻り値」には気をつける,
ということを覚えておけば良いと思います.
*1:既に破壊されたオブジェクトに対する参照のこと。ぶら下がり参照、とも。
*2:ローカル変数の破棄は、構築した順番と逆の順番で行われる以上、ローカル変数に対する参照が生成された時点で、参照先のローカル変数は既に構築されている、言い換えるならば、 参照の生成タイミングは、参照先の変数よりも後になる ので、その参照の破棄タイミングは、参照先のローカル変数よりも先になり、参照変数が参照先を再設定することが出来ない以上、この関係は滅多なことでは破られません。
*3:厳密に言うと、ラムダ式を使って戻り値の型を推論させた場合には、ラムダ式を返す関数を作ることも出来たりします。 http://ideone.com/GjcTx
*4: std::function 等の、実行時多態を行うことで「型を消す」クラス。「型を消す」ことによるメリットは、型が一種類になるので分割コンパイルがしやすくなる、複数の型を std::vector 等に格納できるようになる、などがあります。
*5:ローカル変数を参照キャプチャした場合に限らず、ラムダ式を保持した変数に対して再代入を行うことは出来ません。
*6:生の参照では無理ですが、生ポインタ等を使えば容易に行えます。