変数のキャプチャ
ラムダ式により生成されるクラスの operator()
の関数本体は, this
ポインタやメンバ変数・関数の扱いを除き,ラムダ式の本体と同じコードになる.
auto f = [] (int x, int y) { return x + y; }
struct f_ {
auto operator()(int x, int y) const {
return x + y;
}
};
つまり,ラムダ式の本体の記述には,基本的には関数内クラスで使えるものと同じ物,
例えばグローバル変数や static 変数, constexpr なローカル変数などは自由に使うことができる.
std::string str = "hoge";
int main()
{
constexpr int n = 10;
auto f = [] {
for ( int i = 0; i < n; ++i ) {
std::cout << str << std::endl;
}
};
f();
}
一方で,関数内クラスでローカル変数が使えないのと同じように,
ラムダ式の内部では, constexpr でないローカル変数(や関数の引数)は そのままでは 使えない.
void f(int x)
{
auto g = [] (int y) { return x + y; };
auto h = [] { return g(1) * 2; };
}
それでは不便なので,ラムダ式にはローカル変数のキャプチャという機能が用意されている.
ラムダ式中でのローカル変数へのアクセスは, []
の部分を [&]
にすることで可能になる.
void f(int x)
{
auto g = [&] (int y) { return x + y; };
auto h = [&] { return g(1) * 2; };
std::cout << h() << std::endl;
}
この機能を,ローカル変数の 参照キャプチャ と呼ぶ.
参照キャプチャを使った場合,ラムダ式中で使われている変数は,ラムダ式の外の変数と全く同じものになる.
つまり,ラムダ式の外で変数を変更すると,ラムダ式の中の変数も変更される.
void f(int x)
{
auto g = [&] (int y) { return x + y; };
auto h = [&] { return g(1) * 2; };
std::cout << h() << std::endl;
x = 1;
std::cout << h() << std::endl;
}
逆に,ラムダ式の中で変数を変更すると,ラムダ式の外の変数も変更される.
void f()
{
int x = 0;
auto g = [&] (int y) { x += y; };
g(3);
std::cout << x << std::endl;
g(10);
std::cout << x << std::endl;
}
これは, C++ 以外の多くの言語におけるラムダ式(もしくは関数内関数)の挙動に近い.
function f(x)
local function g(y)
return x + y
end
x = 10
print( g(3) )
end
そのため,他の言語のラムダ式を知っているなら,すんなり理解できるだろう.
このように便利な参照キャプチャであるが,何故 この挙動がデフォルトとなっていないかと言うと,
C++ には言語組み込みの GC が無いため,状況によっては寿命問題が起きる可能性があるからだ.
auto f(int x) {
return [&] (int y) {
return x + y;
};
}
int main()
{
auto g = f(1);
std::cout << g(2) << std::endl;
}
この問題は,関数(ラムダ式を含む)の戻り値として使った場合の他,
std::function
等の type erasure と組み合わせた時などに発生しうる.*1
int main()
{
using function_t = std::function<int(int)>;
std::vector<function_t> vec;
{
int x = 10;
vec.push_back(
[&] (int y) {
return x + y;
}
);
}
for ( auto& f : vec ) {
std::cout << f(2) << std::endl;
}
}
気をつけるべきは「関数の戻り値」や「再代入をはじめとするオブジェクトへの状態変更*2」で,
これらが関わる場合には,参照キャプチャは危険であり,使うことはできない.
そのような場合に用いるのが,変数の コピーキャプチャ である.
これは,ローカル変数をコピーした値をラムダ式内部で保持する機能であり(そのため「値キャプチャ」とも呼ばれる),
独立した変数となるため寿命問題が起きにくい分,参照キャプチャより安全である.*3
使い方は,
auto f(int x, int y) {
return [x, y] (int z) {
return x + y + z;
};
}
int main()
{
auto g = f(1, 2);
std::cout << g(3) << std::endl;
using function_t = std::function<int(int)>;
std::vector<function_t> vec;
{
int x = 10;
vec.push_back(
[x] (int y) {
return x + y;
}
);
}
for ( auto& h : vec ) {
std::cout << h(2) << std::endl;
}
}
のように,使いたい変数の名前を []
の中にカンマ区切りで書けばよい.
ただし,ラムダ式中で使いたい変数がメンバ変数の場合には,そのままでは上手くいかない.
struct Hoge
{
int x;
auto foo() const {
return [x] (int y) { return x + y; };
}
};
この場合, x
をローカルで定義しなおすか,後述の 汎用ラムダキャプチャ構文 を使う必要がある.
struct Hoge
{
int x;
auto foo() const {
auto& x = this->x;
return [x] (int y) { return x + y; };
}
};
いずれにせよ,コンパイルエラーになったら対処する,で問題ない.
また, [=]
と書くことで,変数名のリストを省略することもできる.
auto f(int x, int y) {
return [=] (int z) {
return x + y + z;
};
}
int main()
{
auto g = f(1, 2);
std::cout << g(3) << std::endl;
}
が,コピーキャプチャは後述の理由で効率が良くない上に,
[=]
を使った場合はメンバ変数が暗黙で参照キャプチャされてしまう((厳密に言えば this
を値キャプチャしてるのだが,実質的には参照キャプチャである. *this
が破棄された時点で dangling reference となる.))ため,
筆者は具体的に名前を羅列することを 強く推奨している.
コピーキャプチャによってキャプチャされた変数は,ラムダ式によって生成されたクラスのメンバ変数となる.
つまり,
auto f(int x, int y) {
return [x, y] (int z) {
return x + y + z;
};
}
これは,
auto f(int x, int y) {
struct Internal {
int x; int y;
auto operator()(int z) const {
return x + y + z;
}
};
return Internal{x, y};
}
これと(細かい挙動の差はあるものの)ほとんど同じになる.
メンバ変数は元の変数とは独立しているので,元の変数を変更しても,キャプチャされた値は変化しない.
int main()
{
int x = 0;
auto f = [x] (int y) { return x + y; };
x = 23;
std::cout << f(10) << std::endl;
}
ただし,ポインタ等を使った場合には,(当たり前だが)この限りではない.
int main()
{
int x = 0;
int* p = &x;
auto f = [p] (int y) { return *p + y; };
x = 23;
std::cout << f(10) << std::endl;
}
また,ラムダ式の生成するクラスの operator()
はデフォルトで const 修飾されるため,
キャプチャされた値をラムダ式の内部で変更することは,デフォルトでは不可能である.
int sum = 0;
auto acc = [sum] (int x) {
sum += x;
return sum;
};
どうしても変更したい場合は,()
の後に mutable キーワードを付与する.
int sum = 0;
auto acc = [sum] (int x) mutable {
sum += x;
return sum;
};
とはいえ,上記のような例は スマートポインタを使うなり単に参照キャプチャするなりすれば良い話であり,
標準アルゴリズムと組み合わせた時など,意外と間違いが起きやすいため,
実際のコードで mutable 修飾を使うケースは あまり存在しないし,使わないようにした方が良いだろう.
さて,コピーキャプチャされた変数はメンバ変数となるので,大量のオブジェクトをラムダ式にコピーキャプチャした場合,
または大きなサイズのオブジェクトをコピーキャプチャした場合には,生成されるラムダ式のサイズは大きくなる.
auto f() {
std::array<int, 100000000> a;
for( int i = 0; i < a.size(); ++i ) {
a[i] = std::rand();
}
return [a] (int i) { return a[i]; };
}
int main()
{
auto g = f();
std::cout << sizeof(g) << std::endl;
}
上記のようなケースは, C++11 では, std::shared_ptr
を使うことで解決できる.
auto f() {
using array_t = std::array<int, 100000000>;
auto p = std::make_shared<array_t>();
auto& a = *p;
for( int i = 0; i < a.size(); ++i ) {
a[i] = std::rand();
}
return [p] (int i) { return (*p)[i]; };
}
int main()
{
auto g = f();
std::cout << sizeof(g) << std::endl;
}
変数の数が多い場合には,関数内クラスか std::tuple
と組み合わせた上で std::shared_ptr
を使うと良い.
上記のコードは std::array
ではなく std::vector
を使えばよいと思うかもしれないが,
その場合はコピーキャプチャの際に変数のコピーが発生するため,思うほど速くはならない.((ただし,スタックを食いつぶさないようにする,という点においては, std::vector
の方が良い.))
また, std::shared_ptr
ではなく std::unique_ptr
を使えばよいと思うかもしれないが,
ラムダ式のコピーキャプチャはコピーコンストラクタを要求するため,やはり上手くいかない.
そこで登場するのが, C++14 で追加された 汎用ラムダキャプチャ構文 である.
auto f() {
std::vector<int> vec;
for( int i = 0; i < 100000000; ++i ) {
vec.push_back( std::rand() );
}
return
[vec = std::move(vec)] (int i) {
return vec[i];
};
}
int main()
{
auto g = f();
std::cout << sizeof(g) << std::endl;
}
上記のように, []
の中に 変数名 = 式
のリストをカンマ区切りで書くと,
auto f() {
std::vector<int> vec;
for( int i = 0; i < 100000000; ++i ) {
vec.push_back( std::rand() );
}
struct Internal {
std::vector<int> vec;
auto operator()(int i) const {
return vec[i];
}
};
return Internal{ std::move(vec) };
}
int main()
{
auto g = f();
std::cout << sizeof(g) << std::endl;
}
のような感じの処理になる. 変数の型は auto
で推論されるものと同じになる.
なお,上記のコードで分かるように,ラムダ式の中で使う変数名は,ラムダ式の外の変数名と同じで良い. ((auto x = std::move(x);
とは書けない(厳密に言うなら,書けるけど意図した動作にはならない)のと対照的である.))
これを使えば, std::unique_ptr
のような コピーコンストラクタのないクラスも
ラムダ式の内部で使うことができるようになる他,
ローカル変数に対し,手を加えた上でラムダ式にキャプチャさせることもできる.
auto f(int x) {
return [y = x * 2] (int z) {
return y + z;
};
}
変数名のみを書いたコピーキャプチャと混ぜて使うこともできる.
auto f(int x) {
return [x, y = x * 2] (int z) {
return x + y + z;
};
}
&変数名 = 式
という形で,参照キャプチャを行うこともできる. もちろん寿命には気をつける.
auto f() {
auto p = std::make_shared<Hoge>();
return [p, &x = *p] {
};
}
&x = x
のような形は,単に &x
と書くこともできる.*4
auto f() {
auto p = std::make_unique<Hoge>();
auto& x = *p;
return [&x, p = std::move(p)] {
};
}
なお,ラムダ式のキャプチャは,書いた順番通りに行われるとは限らないため,
上記のコードで [&x = *p, p = std::move(p)]
と書いてしまうと, *p
の参照外しに失敗する可能性がある.
それ以外にも,参照は色々と落とし穴があるので,よく分からない方は使わないのが良いだろう.
* * * * *
と,ラムダ式のキャプチャについて,基本的な部分は解説できたことと思う.
これ以外にも, this
のキャプチャなど,細かいルールは色々とあるのだが,普通に使う分には関係ないので,
そういう細かいルールは明日,最後の章でまとめて紹介してきたい.
蛇足までに,参照キャプチャとコピーキャプチャが分かりにくいという方のため,簡単なコードを用意してみた.
int main() {
int x = 1, y = 2, z = 3;
auto f = [&] { return x + y + z; };
int* p = &z;
auto g = [x, y, z] { return x + y + *p; };
f();
g();
x = 10;
f();
g();
x = 1;
z = 10;
f();
g();
p = &x;
f();
g();
{
int sum = 0;
auto acc = [&] (int i) { sum += i; };
for (int i = 1; i <= 10; ++i) {
acc(i);
}
std::cout << sum << std::endl;
}
{
int sum = 0;
auto acc = [sum] (int i) mutable { sum += i; return sum; };
for (int i = 1; i <= 10; ++i) {
acc(i);
}
std::cout << sum << std::endl;
std::cout << acc(0) << std::endl;
}
}
イメージを掴む助けになれば良いのだが.
* * * * *
今回のまとめ:
- ラムダ式の中ではローカル変数以外は自由に使える. ローカル変数を使いたい場合は,キャプチャをする必要がある
[&]
は参照キャプチャ. ラムダ式の中からローカル変数を操作できるが,寿命に注意
- 変数名を
[]
の中に書くとコピーキャプチャ. 寿命問題を解決できるが効率が悪くなる場合がある
- コピーキャプチャは
std::shared_ptr
と組み合わせると便利である
- C++14 では
[x = std::move(x)]
のように書くことができる. この場合 x
は move される(ムーブキャプチャ)
- 上記の構文は move 以外でも使うことができ,ローカル変数に対して手を加えた上でキャプチャすることが可能
前回と今回で,ラムダ式の機能はだいたい解説することができた.
次回(最終回)では,これらの機能をどのように使えば効果的なのか,という例を紹介し,
おまけとして,完全解説と銘打つ以上は必要と思われる,マニア向けの細かいルールも紹介したい.
私生活が慌ただしくなってきたため,また時間が空いてしまうかもしれないが,最後まで お読み頂ければ幸いである.