概要
クロージャとは、関数オブジェクトの一種であり、その関数が定義された環境の変数をキャプチャして保持できる機能である。
C++ 11から導入されたラムダ式を使用することにより、簡単にクロージャを作成できるようになった。
クロージャの重要な特徴は、関数の外側にある変数を捕捉できることである。
例えば、ある関数の中で定義された変数を、その関数内で作られたクロージャが参照し続けることができる。
これにより、データと振る舞いを一緒にカプセル化することが可能になる。
変数のキャプチャ方法には、主に以下に示すようなものがある。
- 値キャプチャ
- 変数のコピーを保持
- 参照キャプチャ
- 変数への参照を保持
- デフォルトキャプチャ
- 全ての変数を一括でキャプチャ
クロージャは特にアルゴリズムやイベントハンドリング、非同期処理等で重宝される。
例えば、std::sort
の比較関数として使用したり、コールバック関数として利用することができる。
また、クロージャは状態を持つことができるため、関数型プログラミングの考え方を取り入れたコードを記述する場合にも便利である。
関数オブジェクトとして扱えるため、STLアルゴリズムとも相性が良く、コードの可読性と保守性の向上に貢献する。
ただし、参照キャプチャを使用する際は、参照先の変数のライフタイムに注意する必要がある。
参照先の変数が破棄された後にクロージャを使用すると、未定義動作を引き起こす可能性がある。
基本的な作成
必ずしも、ラムダ式をコピーキャプチャかつミューテーブルにする必要はない。
コピーキャプチャ [=]
とは、キャプチャした変数の値をクロージャ内部にコピーして保持するものである。
元の変数との関係が切れるため、安全に使用することができる。
ただし、メモリ使用量が増える可能性がある。
mutable
キーワードとは、デフォルトでは、コピーキャプチャされた変数はクロージャ内で変更できない。
mutableキーワードを使用する場合、コピーキャプチャした変数を変更できる。
ただし、これはクロージャ内部のコピーを変更するだけであるため、元の変数は変更されない。
以下に示す場合は、mutable
キーワードは不要である。
- キャプチャした変数を読み取りのみで使用する場合
- 参照キャプチャ
[&]
を使用している場合 - クロージャ内で状態を保持する必要がない場合
ただし、以下に示す場合はmutable
が必要になる。
- コピーキャプチャした変数をクロージャ内で変更する場合
- クロージャ自体が状態を持つ必要がある場合
クロージャの例 1
以下の例では、func関数内でローカル変数xを定義して、それをラムダ式でキャプチャしている。
ラムダ式は、変数xの値をコピーキャプチャ [=] して保持する。
mutableキーワードにより、キャプチャしたxの値を変更可能にしている。
このラムダ式 (クロージャ) は、func関数の実行が終わった後も、キャプチャしたxの値を保持し続ける。
f1とf2は別々のクロージャインスタンスとなり、それぞれが独自のxのコピーを持つ。
そのため、f1の呼び出しではf1が保持するxが1から3まで増加、f2の呼び出しでは新しいクロージャインスタンスが生成されて、そのxが1から3まで増加する。
このように、クロージャは状態 (この場合はxの値) を保持でき、その状態は他のクロージャインスタンスとは独立しているという特徴を示している。
#include <iostream>
auto func()
{
int x = 0;
return [=]() mutable -> void {
x++;
std::cout << x << std::endl;
};
}
int main()
{
auto f1 = func();
f1(); // 出力: 1
f1(); // 出力: 2
f1(); // 出力: 3
auto f2 = func();
f2(); // 出力: 1
f2(); // 出力: 2
f2(); // 出力: 3
}
以下の例のように、環境を共有する複数の処理を行う場合は、引数の値により条件分岐することができる。
なお、参照キャプチャは、オブジェクトを複数生成する場合は使用できない。
#include <iostream>
auto func()
{
int x = 0;
return [=](std::size_t mode = 0) mutable -> void {
switch(mode) {
case 1:
++x;
std::cout << x << std::endl;
break;
case 2:
--x;
std::cout << x << std::endl;
break;
default:
std::cout << x << std::endl;
break;
}
};
}
int main()
{
auto f1 = func();
auto f2 = func();
f1(1); // 出力: 1
f1(2); // 出力: 0
f1(1); // 出力: 1
f1(2); // 出力: 0
f1(); // 出力: 0
f2(1); // 出力: 1
f2(1); // 出力: 2
f2(2); // 出力: 1
f2(2); // 出力: 0
f2(); // 出力: 0
}
処理を関数ごとに分ける場合、bind関数等は新しく関数を生成しているため、環境が共有されず使用できない。
処理を分ける場合は、上記のf1関数を生成した後、以下の処理を記述する。
auto inc = [&]() { return f1(1); };
auto dec = [&]() { return f1(2); };
inc();
dec();