最終更新日時:
が更新

履歴 編集

4. Using the library

このセクションの目的はλライブラリの基本的な機能を紹介することである。 例外事項や特別な場合も非常にたくさんあるが、それらの議論は後のセクションにまわすことにする。

4.1. Introductory Examples

このセクションでは、STL アルゴリズムの呼出しにおける、BLL のλ式の基本的な例を示す。 いくつかの単純な式から始めて、徐々に発展させていく。 まず、コンテナ( 例えば list ) の要素を 1 に初期化してみる。

list<int> v(10);
for_each(v.begin(), v.end(), _1 = 1);

_1 = 1 という式は、v の全ての要素に対して 1 を割当てるλファンクタを生成する。 [1]

次に、ポインタのコンテナを作成し、そのポインタを 最初の コンテナ v の要素を指すようにする。

vector<int*> vp(10);
transform(v.begin(), v.end(), vp.begin(), &_1);

&_1という式は、v のそれぞれの要素のアドレスを取得する関数オブジェクトを生成する。 そして、そのアドレスは対応する vp の要素に割当てられる。

次のコードは v の要素の値を変化させる。 それぞれの要素に対して、関数 foo が呼ばれる。 foo には、各要素の値が引数として渡され、その結果は引数として渡された要素に割当てられる。

int foo(int);
for_each(v.begin(), v.end(), _1 = bind(foo, _1));

次では、vp の要素のソートを行う。

sort(vp.begin(), vp.end(), *_1 > *_2);

この sort の呼出しでは、要素をポインタの指す内容の降順でソートする。

最後に、次の for_each の呼出しでは、ソートされた vp の要素の指す内容を改行区切りで出力する。

for_each(vp.begin(), vp.end(), cout << *_1 << '\n');

λ式の部分式で通常の式( λ式でない )ものは即座に評価されることに注意しなければならない。 そうしないと、予期しない結果を導いてしまう。 例えば、先の例は次のように書換えてみると、

for_each(vp.begin(), vp.end(), cout << '\n' << *_1);

部分式 cout << << '\n' は即座に評価されてしまう。 その結果、一つの改行とそれに続いて vp の要素が出力されてしまう。 BLL では関数 constant と関数 var が提供されており、それぞれ定数と変数をλ式へと変換する。 これらの関数を使用することにより、部分式が即座に評価されてしまうことを防止できる。

for_each(vp.begin(), vp.end(), cout << constant('\n') << *_1);

これらの関数は Section 5.5 でより詳しく記述する。

4.2. Parameter and return types of lambda functors

λファンクタの呼出しの間は、実引数はプレースホルダーとして抽象化されている。 プレースホルダーは実引数の型には影響を与えない。 基本的には、抽象化されたλ式が C++ の有効な式であるかぎり、λ関数はいかなる型の引数をともなっても呼ぶことができる。 例えば、 _1 + _2 は、二項λファンクタを生成する。 このファンクタはいかなる型 A と 型 B の二つのオブジェクトと共に呼出すことができる。 これらの型には operator+(A,B) が定義されていればよい。 (そして、BLL がその演算子の返り値を識別できればよい。 詳しくは以下を見よ。)

C++ には式の型を問合せるメカニズムが存在しない。 しかし、この正確な機能は C++ のλ式の実装には極めて重大である。 よって、BLL はちょっとした複雑な型推論システムを内包している。 そこでは、λ関数の返り値の型を推論するために、特性クラスの集合を使用している。 その推論システムは組み込み型や、標準ライブラリの型がオペランドである多くの式がオペランドである式を扱う。 多くのユーザ定義型も、返り値の型の決定における通常の慣例に従ってユーザが演算子を定義している限りは、うまく対応できている。

しかしながら、返り値の型を推論できない場合が存在する。 例えば、次のように定義をしたとすると、

C operator+(A, B);

次のλファンクタの呼出しは、返り値の型を推論できないために失敗する。

A a; B b; (_1 + _2)(a, b);

これを解決するには二つの代替案がある。 一つ目は、BLL の型推論システムを独自の型に対応するために拡張することである。 (詳しくは Section 6 を見よ) 二つ目の方法は次のように特別なλ式 ret を使用することである。

A a; B b; ret<C>(_1 + _2)(a, b);

このλ式は型推論システムに代わって、返り値の型を決定する。 (詳しくは Section 5.4 を見よ)

bind 式に関しては、返り値の型はテンプレート引数として次のように指定することができる。

bind<int>(foo, _1, _2);

4.3. About actual arguments to lambda functors

一般的な実引数の制限は、実引数は constでない右辺値ではないということである。 例えば、

int i = 1; int j = 2;
(_1 + _2)(i, j); // ok
(_1 + _2)(1, 2); // error (!)

である。 この制限はそれほど悪いものではない。 λファンクタはたいていの場合、STL のアルゴリズム中で呼出されるため、引数は参照外しのイテレータであり、参照外しの操作が右辺値を返すことはほとんどないからである。 そして、右辺値を返すような場合には、Section 5.9.2で述べるような代替案がある。

4.4. Storing bound arguments in lambda functions

デフォルトの動作では、束縛された引数の const な一時的なコピーは、λファンクタ中に格納される。 つまり、束縛された引数の値は、λ関数の生成時に固定され、かつ、λ関数オブジェクトのライフタイムの間は定数であり続けるということである。 例えば、

int i = 1;
(_1 + i)(i = 2);

最後の行の式の値は 4 ではなく、3 である。 言い換えると、λ式 _1 + ilambda x.x+i ではなく、lambda x.x+1 というλ関数を生成する。

先に述べたように、これはあくまでデフォルトの動作であって、例外もある。 正確な規則は次のようになる。

  • プログラマは refcref のラッパ[ref]を使用することにより、格納のメカニズムを操作することができる。 引数を refcrefでラップすることにより、引数をそれぞれ参照として、const への参照として格納するように指定することができる。 例えば、先の例の変数 iref でラップして書き直せば、lambda x.x+iというλ式を作成し、最後の行の式の値は 4 となる。

i = 1;
(_1 + ref(i))(i = 2);

refcrefvarconstant とは異ることに注意しなさい。 後者は、λファンクタを生成するのに対し、前者はそうではない。 例えば、

int i;
var(i) = 1; // ok
ref(i) = 1; // not ok, ref(i) is not a lambda functor

となる。 関数 refcref はほとんど歴史的な理由からのみ存在している。 そして、refvarcrefconstant_refで置き換えることができる。 詳しくは Section 5.5 を見よ。 refcref は Boost の基本的な目的のユーティリティー関数であるので、boost ネームスペースに直接定義されている。

  • 配列型はコピーすることはできず、デフォルトの動作では、const な参照として格納される。
  • いくつかの式については、参照として引数を格納するほうが納得がいく。 例えば、λ式i += _1の意図は明らかに、λファンクタの呼出しにより、一時的なコピーではなく、変数 i そのものの値に影響を与えることである。 別な例を挙げると、ストリームの演算子は const でない参照として最左の引数をとる。 正確な規則は次のようになる。
    • 複合代入演算子(+=*= のようなもの)の左側の引数は、const でない参照として格納される。
    • << の左の引数が basic_ostreamインスタンス化 から派生していた場合や、 >> の左の引数が basic_istreamインスタンス化 から派生した場合には、その引数は constでない参照として格納される。 その他の型に関しては、引数はコピーとして格納される。
    • ポインタの算術式においては、const でない配列型は、constでない参照として格納される。 これは、ポインタの算術計算によって、const でない配列が const になるのを防ぐためである。

[1] 厳密には、C++ 標準規格では、for_each変更のない連続操作 であり、for_each へ渡す関数オブジェクトはその引数をを変更すべきでないと定義されている。 この引数に対する要求は不必要に厳しすぎる。 なぜなら、イテレータが mutable である限り、for_each は引数への副作用のある関数オブジュクトも処理できるからである。 それでもやはり、std::for_each の機能を持つが、引数に関する一層きめの細かい要求の関数テンプレートを提供することは簡単である。