クロージャを使ってみる
ここで、放置されていたキャプチャ句についてようやく触れることにします。
関数オブジェクトとクロージャ
実は、関数オブジェクトというのは「オブジェクト」というだけあって、クラス定義を伴います。ラムダ式が現れると、以下のような仮のクラス(構造体)を定義し、そのメンバとして関数(operator())を実装するような処理が、なじみのある書き方で生成されるのです(あくまでもイメージです。autoやdecltypeなどの型推論関連キーワードについては連載第2回を読んでください)。
struct _F {
auto operator()(int x, int y) const -> decltype(x * y)
{
return x + y;
}
};
これを踏まえると、ラムダ式というのはクラスと関数を生成するための構文と言えそうです。C++には、そういうのがたくさんあるんですけどね、テンプレートとか。なお、このときのクラスはクロージャ型、インスタンスはクロージャオブジェクトと呼ばれるそうです。そこでクロージャって何?となると思いますが、ラムダ式とほぼ同意に語られることが多いように感じます。なので、以降は基本的にラムダ式という言葉を使います(書き手の都合で、クロージャを引っ張り出すかも知れません)。
クロージャ(closure)とは、「閉鎖」とか「閉じるもの」といった意味です。なんか、字面から内向きな感じがしますね。数学の集合論で「閉包」という概念がありますが、これはクロージャです。閉じて包む、こっちの方が意味的に通りやすいのではないかと思ってます。このクロージャ、放置中のキャプチャと密接に絡んできます。というか、切っても切れない仲です。閉包という言葉と絡めて、キャプチャをやっていきましょう。
キャプチャと環境
キャプチャとは、ラムダ式の外にある変数(これは、外部変数と呼ばれます。自由変数と呼ぶこともあるようです)をラムダ式の中で使うことです。冒頭で、ラムダ式の書式を紹介しましたが、そこにあった[キャプチャ句]はラムダ導入子やラムダ紹介子と呼ばれ、ラムダ式の中に外部変数を導入する(取り込む=キャプチャする)役割を持ちます。簡単な例で、キャプチャ句を見てみましょう。以下のリストは、コピーキャプチャという最も基本となるキャプチャです。
int i = 100; (1)
auto func = [i]() { return i; }; // iをコピーキャプチャ
cout << func() << endl; // 実行結果:100
i = 200; (2)
cout << func() << endl; // 実行結果:100
変数iはラムダ式の外で宣言されていますが、キャプチャ句の中に記述され、さらに関数の中で参照されています。コピーキャプチャの名前の通り、変数iはコピーされてラムダ式に渡ります。コピーされるので、並列実行される非同期関数などに使うと便利そうなイメージですね。
ここで、キャプチャの重要な性質があります。上記リストの実行結果を見ると、2回の関数呼び出しで戻り値は等しくなっています。なぜ?と思いますよね。(1)で変数iは100になり、(2)で200になっているからです。キャプチャでは、ラムダ式の定義時の環境(出ました、環境です)をキャプチャするからです。つまり、ラムダ式定義時のiの値100をずっと持っているような感じになります。200に値を変えても、もはや手遅れなのです。環境というのは、ラムダ式の置かれた境遇(つまりそのときはiは100だった)を表すのです。
[NOTE]そもそもキャプチャが必要なわけ
そもそもなぜキャプチャするの? という疑問が湧きませんか? ラムダ式の関数を呼び出すとき、ラムダ式定義時の環境にあった情報が必要になるときがあります。とはいえその情報は、ラムダ式の関数呼び出し時にはアクセスできるとは限りません。例えば、設定情報を保持するオブジェクトでしょうか。呼び出し時に渡せるのは、あくまでもそのスコープにある値だけです。そのため、キャプチャという仕組みが必要になってくるのです。
キャプチャの種類
コピーキャプチャを紹介しましたが、キャプチャには、これを含めて表に示すような種類があります。
| キャプチャ導入子の書き方 | 概要 |
|---|---|
| [] | 何もキャプチャしない |
| [c] | 変数cをコピーしてラムダ式で使用する(コピーキャプチャ) |
| [&c] | 変数cを参照してラムダ式で使用する(参照キャプチャ) |
| [=] | 環境にある変数をコピーしてラムダ式で使用する(キャプチャデフォルトモードをコピーとする) |
| [&] | 環境にある変数を参照してラムダ式で使用する(キャプチャデフォルトモードを参照とする) |
| [=, &c] | キャプチャデフォルトモードはコピーで変数cのみ参照してラムダ式で使用する |
| [&, c] | キャプチャデフォルトモードは参照で変数cのみコピーしてラムダ式で使用する |
| [*this] | *thisのメンバをコピーしてラムダ式で使用する(C++ 17) |
| [*this, &c] | *thisのメンバをコピーして変数cのみ参照してラムダ式で使用する(C++ 17) |
| [this] | *thisのメンバを参照してラムダ式で使用する |
| [this, c] | *thisのメンバを参照し変数cのみコピーしてラムダ式で使用する |
キャプチャデフォルトモードとは、環境にある外部変数をどのようにキャプチャするか?と指定するものです。キャプチャデフォルトモードを指定すると、環境にある全ての外部変数がコピー、参照されます(詳細はあとで)。
thisを使うと、ラムダ式を定義したクラスのメンバ変数をキャプチャできます。基本的に参照となり、個別の外部変数のキャプチャも指定できます。
参照キャプチャについても以下のリストで見てみましょう。
int j = 100;
auto func_ref = [&j]() { return j; }; // jを参照キャプチャ
cout << func_ref() << endl; // 実行結果:100
j = 200;
cout << func_ref() << endl; // 実行結果:200
先ほどの実行結果と異なり、2回目の呼び出しの結果が変化しています。それは、ラムダ式がキャプチャしたのは参照であるので、同じ参照でも参照先の値が変われば評価結果も変化するからです。また、ラムダ式の呼び出しにあたり、参照先が消滅している可能性もあります。このように、コピーキャプチャと参照キャプチャは性質が異なるので、用途をそれに合わせて変える必要がありますね。
環境についても掘り下げます。表で、「環境にある変数」と書きましたが、ずいぶんと漠然としていますね。環境って、どこまでを指すのでしょうか? 「私たちの住む環境」という表現もずいぶんと大ざっぱですが、それと似たようなニュアンスですね。ここでの環境は、ラムダ式の定義されたスコープを指します。当たり前と言ってしまえば当たり前ですね。際限なくキャプチャ対象を広げていったら、グローバル変数までたどり着いてしまった、ということにもなりかねません。
環境の解釈を踏まえて、キャプチャデフォルトモードを見てみます。キャプチャデフォルトモードを指定すると、環境にある外部変数を全てコピーあるいは参照するという仕様になっています。ただし、実際にはラムダ式内で使用しているもののみ、となります。ラムダ式中で使用されなければキャプチャする意味もないわけで、自然な動きですね。
[NOTE]キャプチャの一般化
C++ 14では、キャプチャする対象がスコープに存在していなくてもよくなっています。キャプチャ句で、以下のように記述すればその場で変数を生成してそれをキャプチャできます。これをキャプチャの一般化と呼びます。
auto func = [x = 200]() { return x; }
変数xの型は初期化する式から型推論されます。
