初期化ラムダキャプチャーはラムダキャプチャーする変数の名前と式を書くことができる機能だ。
ラムダ式は書かれた場所から見えるスコープの変数をキャプチャーする。
int main()
{
int x = 0 ;
auto f = [=]{ return x ; } ;
f() ;
}
初期化ラムダキャプチャーはラムダキャプチャーに初期化子を書くことができる機能だ。
int main()
{
int x = 0 ;
[ x = x, y = x, &ref = x, x2 = x * 2 ]
{// キャプチャーされた変数を使う
x ;
y ;
ref ;
x2 ;
} ;
}
初期化ラムダキャプチャーは、"識別子 = expr
" という文法でラムダ導入子[]
の中に書く。するとあたかも"auto
識別子 = expr ;
"と書いたかのように変数が作られる。これによりキャプチャーする変数の名前を変えたり、まったく新しい変数を宣言することができる。
初期化ラムダキャプチャーの識別子の前に&
を付けると、リファレンスキャプチャー扱いになる。
int main()
{
int x = 0 ;
[ &ref = x ]()
{
ref = 1 ;
}() ;
// xは1
}
初期化ラムダキャプチャーが追加された理由には変数の名前を変えたりまったく新しい変数を導入したいという目的の他に、非static
データメンバーをコピーキャプチャーするという目的がある。
以下のコードには問題があるが、わかるだろうか。
struct X
{
int data = 42 ;
auto get_closure_object()
{
return [=]{ return data ; } ;
}
} ;
int main()
{
std::function< int() > f ;
{
X x ;
f = x.get_closure_object() ;
}
std::cout << f() << std::endl ;
}
X::get_closure_object
はX::data
を返すクロージャーオブジェクトを返す。
auto get_closure_object()
{
return [=]{ return data ; } ;
}
これを見ると、コピーキャプチャーである[=]
を使っているので、data
はクロージャーオブジェクト内にコピーされているように思える。しかし、ラムダ式は非static
データメンバーをキャプチャーしてはいない。ラムダ式がキャプチャーしているのはthis
ポインターだ。上のコードと下のコードは同じ意味になる。
auto get_closure_object()
{
return [this]{ return this->data ; } ;
}
さて、main
関数をもう一度見てみよう。
int main()
{
// クロージャーオブジェクトを代入するための変数
std::function< int() > f ;
{
X x ; // xが構築される
f = x.get_closure_object() ;
// xが破棄される
}
// すでにxは破棄された
// return &x->dataで破棄されたxを参照する
std::cout << f() << std::endl ;
}
なんと、すでに破棄されたオブジェクトへのリファレンスを参照してしまっている。これは未定義の動作だ。
初期化ラムダキャプチャーを使えば、非static
データメンバーもコピーキャプチャーできる。
auto get_closure_object()
{
return [data=data]{ return data ; } ;
}
なお、ムーブキャプチャーは存在しない。ムーブというのは特殊なコピーなので初期化ラムダキャプチャーがあれば実現できるからだ。
auto f()
{
std::string str ;
std::cin >> str ;
// ムーブ
return [str = std::move(str)]{ return str ; } ;
}
機能テストマクロは__cpp_init_captures
, 値は201304。