C++11のラムダ式とは何ですか?どのような場合に使用するのですか?ラムダ式が導入される前には不可能だった、どのような種類の問題を解決しますか?
いくつかの例や使用例があると便利です。
C++には,std::for_each
やstd::transform
といった便利な汎用関数が用意されており,非常に重宝します.しかし,残念ながら,これらの関数は使いづらく,特に適用したいfunctorが特定の関数に固有のものである場合には,非常に使いづらいことがあります.
#include <algorithm>
#include <vector>
namespace {
struct f {
void operator()(int) {
// do something
}
};
}
void func(std::vector<int>& v) {
f f;
std::for_each(v.begin(), v.end(), f);
}
もしあなたが f
を一度だけ、しかもその特定の場所でしか使わないのであれば、些細で一度きりのことをするためにクラス全体を書くのはやりすぎのように思えます。
C++03 では、ファンクタをローカルに保つために、次のように書きたくなるかもしれません。
void func2(std::vector<int>& v) {
struct {
void operator()(int) {
// do something
}
} f;
std::for_each(v.begin(), v.end(), f);
}
しかし、C++03 では f
を テンプレート 関数に渡すことはできません。
C++11 ではラムダが導入され、「構造体 f」の代わりにインラインの無名ファンクタを書くことができるようになりました。小さな簡単な例では、この方法は読みやすく(すべてを一箇所にまとめておくことができる)、メンテナンスも簡単になる可能性があります。
void func3(std::vector<int>& v) {
std::for_each(v.begin(), v.end(), [](int) { /* do something here*/ });
}
ラムダ関数は無名のファンクタのための単なる構文上の糖である。
単純なケースでは、ラムダの戻り値の型はあなたのために推論されます。
void func4(std::vector<double>& v) {
std::transform(v.begin(), v.end(), v.begin(),
[](double d) { return d < 0.00001 ? 0 : d; }
);
}
しかし、より複雑なラムダを書き始めると、すぐにコンパイラが戻り値の型を推論できないケースに遭遇します。
void func4(std::vector<double>& v) {
std::transform(v.begin(), v.end(), v.begin(),
[](double d) {
if (d < 0.0001) {
return 0;
} else {
return d;
}
});
}
これを解決するために、-> T
を使用して、ラムダ関数の戻り値の型を明示的に指定することができます。
void func4(std::vector<double>& v) {
std::transform(v.begin(), v.end(), v.begin(),
[](double d) -> double {
if (d < 0.0001) {
return 0;
} else {
return d;
}
});
}
これまでのところ、ラムダに渡されたもの以外のものをラムダ内で使用していませんが、ラムダ内で他の変数を使用することもできます。他の変数にアクセスしたい場合は、キャプチャ句(式の []
)を使用することができますが、ここまでの例では使用していません。
void func5(std::vector<double>& v, const double& epsilon) {
std::transform(v.begin(), v.end(), v.begin(),
[epsilon](double d) -> double {
if (d < epsilon) {
return 0;
} else {
return d;
}
});
}
参照と値の両方でキャプチャすることができ、それぞれ &
と =
を使って指定します。
[&epsilon]
参照でキャプチャ[&]
は、ラムダで使用されるすべての変数を参照でキャプチャします。[=]
ラムダで使用されているすべての変数を値でキャプチャします。[&, epsilon]
は、[&]と同様に変数をキャプチャしますが、値としてイプシロンを使用します。[=, &epsilon]
は、[=]と同じように変数をキャプチャしますが、参照ではイプシロンを使用します。生成されたoperator()
は,デフォルトではconst
であり,これは,キャプチャにアクセスするときに,デフォルトでconst
になることを意味しています。しかし,ラムダを mutable
としてマークして ,生成される operator()
が const
でないことを要求することができます.
C++のラムダ関数の概念は、ラムダ計算と関数型プログラミングに由来しています。ラムダとは名前のない関数のことで、再利用が不可能で名前をつける価値のない短いコードの断片に(理論ではなく実際のプログラミングで)有効です。
C++では、ラムダ関数は次のように定義されています。
[]() { } // barebone lambda
あるいは、その栄光のために
[]() mutable -> T { } // T is the return type, still lacking throw()
[]がキャプチャリスト、
()が引数リスト、
{}`が関数本体です。
キャプチャリストは、ラムダの外側から何をどのようにして関数本体の中で利用するかを定義します。 これは次のいずれかになります。
1.値。[x] 2. 参照 [&x] 。 3. 現在スコープ内にある任意の変数を参照して [&] とする 4. 3と同じだが、値である [=] 。
上記のいずれかをカンマで区切ったリスト [x, &y]
に混在させることができます。
引数リストは、他のC++関数と同じです。
ラムダが実際に呼び出されたときに実行されるコードです。
ラムダが1つのリターンステートメントしか持たない場合、リターンタイプは省略可能で、暗黙の型である decltype(return_statement)
を持ちます。
ラムダが mutable とマークされている場合 (例: []() mutable { }
) は、value で取得した値を mutate することができます。
ISO標準で定義されたライブラリはラムダの恩恵を大きく受け、ユーザがアクセス可能なスコープにある小さなファンクタでコードを乱雑にする必要がないため、ユーザビリティが数段向上します。
C++14 では、ラムダはさまざまな提案によって拡張されています。
キャプチャリストの要素を =
で初期化できるようになりました。これにより、変数の名前を変更したり、移動によるキャプチャが可能になります。規格から抜粋した例です。
int x = 4;
auto y = [&r = x, x = x+1]()->int {
r += 2;
return x+2;
}(); // Updates ::x to 6, and initializes y to 7.
とWikipediaから引用したもので、std::move
でキャプチャする方法を示しています。
auto ptr = std::make_unique<int>(10); // See below for std::make_unique
auto lambda = [ptr = std::move(ptr)] {return *ptr;};
ラムダがジェネリックになりました(auto
は、T
が周囲のスコープのどこかで型テンプレートの引数になっていれば、ここではT
と同じになります)。
Tが周囲のスコープのどこかにある型テンプレートの引数であれば、ここでは
T` と同等です)。)
auto lambda = [](auto x, auto y) {return x + y;};
C++14 では、すべての関数で戻り値の型を推論できるようになっており、それは return expression;
という形式の関数に限定されません。これはラムダにも拡張されています。
ラムダ式は通常、アルゴリズムをカプセル化して別の関数に渡すために使用されます。 しかし、ラムダ式を定義した直後に実行することも可能です。
[&](){ ...your code... }(); // immediately executed lambda expression
は機能的には以下と同等です。
{ ...your code... } // simple code block
これにより、ラムダ式は複雑な関数をリファクタリングするための強力なツールとなります。 上記のように、コードセクションをラムダ関数でラップすることから始めます。 明示的なパラメータ化のプロセスは、各ステップの後に中間テストを行いながら徐々に実行することができます。 コードブロックが完全にパラメータ化されたら(&
の削除でわかるように)、コードを外部に移動して通常の関数にすることができます。
同様に、ラムダ式を使って、アルゴリズムの結果に基づいて変数を初期化することができます...
int a = []( int b ){ int r=1; while (b>0) r*=b--; return r; }(5); // 5!
プログラムロジックを分割する**方法として、ラムダ式を別のラムダ式の引数として渡すことも便利です...
[&]( std::function<void()> algorithm ) // wrapper section
{
...your wrapper code...
algorithm();
...your wrapper code...
}
([&]() // algorithm section
{
...your algorithm code...
});
また、ラムダ式では名前付きの入れ子の関数を作ることができ、ロジックの重複を避ける便利な方法となります。 また、名前付きラムダを使うと、自明でない関数を他の関数のパラメータとして渡すときに、(無名のインラインラムダに比べて)目が少し楽になる傾向があります。 *注意:閉じた中括弧の後のセミコロンをお忘れなく。
auto algorithm = [&]( double x, double m, double b ) -> double
{
return m*x+b;
};
int a=algorithm(1,2,3), b=algorithm(4,5,6);
その後のプロファイリングで、関数オブジェクトの初期化のオーバーヘッドが大きいことが判明した場合、これを通常の関数として書き直すこともできます。