ラムダ:C ++ 11からC ++ 20まで。 パート1

こんにちは、友達。 本日、記事「Lambdas:from C ++ 11 to C ++ 20」の最初の部分の翻訳を準備しました。 この資料の発行は、明日から開始されるコース「C ++ Developer」の開始に捧げられます。

ラムダ式はC ++ 11で最も強力な追加機能の1つであり、新しい言語標準ごとに進化を続けています。 この記事では、彼らの歴史を振り返り、現代のC ++のこの重要な部分の進化を見ていきます。



2番目の部分は次の場所にあります。
ラムダ:C ++ 11からC ++ 20、パート2

エントリー

ローカルC ++ユーザーグループ会議で、ラムダ式の「履歴」に関するライブプログラミングセッションがありました。 会話は、C ++の専門家であるTomasz Kami byskiが主導しました( ThomasのLinkedinプロファイルを参照 )。 イベントは次のとおりです。

Lambdas:C ++ 11からC ++ 20-C ++ User Group Krakow

私はトーマスからコードを受け取り(彼の許可を得て!)、説明して別の記事を作成することにしました。

C ++ 03とコンパクトなローカル関数式の必要性を調べることから始めます。 次に、C ++ 11およびC ++ 14に進みます。 シリーズの第2部では、C ++ 17の変更点を確認し、C ++ 20で何が起こるかを見ていきます。

C ++ 03のラムダ

最初から、STL std::sortなどのSTLのstd::algorithms 、呼び出されたオブジェクトを取得し、コンテナ要素で呼び出すことができます。 ただし、C ++ 03では、これには関数とファンクターへのポインターのみが含まれていました。

例:

 #include <iostream> #include <algorithm> #include <vector> struct PrintFunctor { void operator()(int x) const { std::cout << x << std::endl; } }; int main() { std::vector<int> v; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), PrintFunctor()); } 

実行中のコード: @Wandbox

しかし、問題は、アルゴリズム呼び出しのスコープではなく、異なるスコープで別個の関数またはファンクターを作成する必要があることでした。

潜在的な解決策として、ローカルファンクタクラスの作成を検討することをお勧めします-C ++は常にこの構文をサポートしているためです。 しかし、それはうまくいきません...

このコードを見てください:

 int main() { struct PrintFunctor { void operator()(int x) const { std::cout << x << std::endl; } }; std::vector<int> v; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), PrintFunctor()); } 

-std=c++98コンパイルしてみてください-std=c++98次のエラーが表示されます。

 error: template argument for 'template<class _IIter, class _Funct> _Funct std::for_each(_IIter, _IIter, _Funct)' uses local type 'main()::PrintFunctor' 

実際、C ++ 98/03では、ローカルタイプのテンプレートのインスタンスを作成できません。
これらすべての制限のため、委員会は、「インプレース」...「ラムダ式」を作成して呼び出すことができる新しい機能の開発を開始しました!

C ++ 11の最終バージョンであるN3337を見ると、 ラムダの別のセクション[expr.prim.lambda]が表示されます。

C ++ 11の隣

ラムダは賢明に言語に追加されたと思います。 新しい構文を使用しますが、コンパイラはそれを実際のクラスに「拡張」します。 したがって、厳密に型付けされた実際の言語のすべての利点(および場合によっては欠点)があります。

以下は、対応するローカルファンクターオブジェクトも示す基本的なコード例です。

 #include <iostream> #include <algorithm> #include <vector> int main() { struct { void operator()(int x) const { std::cout << x << '\n'; } } someInstance; std::vector<int> v; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), someInstance); std::for_each(v.begin(), v.end(), [] (int x) { std::cout << x << '\n'; } ); } 

例: @WandBox

CppInsightsも確認できます。これは、コンパイラがコードをどのように拡張するかを示しています。

この例を見てください:

CppInsighs:ラムダテスト

この例では、コンパイラーは以下を変換します。

 [] (int x) { std::cout << x << '\n'; } 


これに似たものに(簡略化された形式):

 struct { void operator()(int x) const { std::cout << x << '\n'; } } someInstance; 

ラムダ式の構文:

 [] () { ; } ^ ^ ^ | | | | | : mutable, exception, trailing return, ... | | |   |      

始める前のいくつかの定義:

[expr.prim.lambda#2]から

ラムダ式を評価すると、一時的なprvalueになります。 この一時オブジェクトは、 クロージャオブジェクトと呼ばれます

[expr.prim.lambda#3]から

ラムダ式のタイプ(クロージャオブジェクトのタイプでもあります)は、 クロージャタイプと呼ばれるクラスの一意の名前のない非ユニオンタイプです。

ラムダ式のいくつかの例:

例:

 [](float f, int a) { return a*f; } [](MyClass t) -> int { auto a = t.compute(); return a; } [](int a, int b) { return a < b; } 

ラムダタイプ

コンパイラは各ラムダに対して一意の名前を生成するため、事前にそれを知ることはできません。

 auto myLambda = [](int a) -> double { return 2.0 * a; } 

さらに[expr.prim.lambda]
ラムダ式に関連付けられたクロージャータイプには、リモート([dcl.fct.def.delete])デフォルトコンストラクターとリモート割り当て演算子があります。

したがって、次のように書くことはできません。

 auto foo = [&x, &y]() { ++x; ++y; }; decltype(foo) fooCopy; 

これにより、GCCで次のエラーが発生します。

 error: use of deleted function 'main()::<lambda()>::<lambda>()' decltype(foo) fooCopy; ^~~~~~~ note: a lambda closure type has a deleted default constructor 

オペレーターを呼び出す

ラムダの本体に入れたコードは、対応するクロージャータイプのoperator()コードに「変換」されます。

デフォルトでは、これは組み込みの定数メソッドです。 パラメーターを宣言した後にmutableを指定することで変更できます。

 auto myLambda = [](int a) mutable { std::cout << a; } 

定数メソッドは、空のキャプチャリストのないラムダの「問題」ではありませんが、何かをキャプチャしたいときに重要です。

キャプチャー

[]はラムダを導入するだけでなく、キャプチャされた変数のリストも含みます。 これはキャプチャリストと呼ばれます。

変数をキャプチャすることにより、この変数のコピーメンバーをクロージャータイプで作成します。 次に、ラムダ本体内でアクセスできます。

基本的な構文は次のとおりです。


例:

 int x = 1, y = 1; { std::cout << x << " " << y << std::endl; auto foo = [&x, &y]() { ++x; ++y; }; foo(); std::cout << x << " " << y << std::endl; } 

ここで完全な例を試してみることができます: @Wandbox

[=]または[&]指定すると便利な場合があります-自動ストレージ内のすべての変数をキャプチャするため、変数を明示的にキャプチャする方が明確です。 したがって、コンパイラは望ましくない効果について警告することができます(たとえば、グローバル変数および静的変数に関する注意を参照)

Scott MeyersによるEffective Modern C ++の段落31の「デフォルトのキャプチャモードを回避する」も参照してください。

そして重要な引用:
C ++クロージャは、キャプチャされたリンクの寿命を延ばしません。


可変

デフォルトでは、クロージャタイプ演算子()は定数であり、ラムダ式の本体内のキャプチャされた変数を変更することはできません。
この動作を変更する場合は、パラメーターリストの後にmutableキーワードを追加する必要があります。

 int x = 1, y = 1; std::cout << x << " " << y << std::endl; auto foo = [x, y]() mutable { ++x; ++y; }; foo(); std::cout << x << " " << y << std::endl; 

上記の例では、xとyの値を変更できますが、これらは接続されたスコープからのxとyのコピーにすぎません。

グローバル変数のキャプチャ

グローバル値があり、ラムダで[=]を使用する場合、グローバル値もvalueによってキャプチャされると考えるかもしれませんが、そうではありません。

 int global = 10; int main() { std::cout << global << std::endl; auto foo = [=] () mutable { ++global; }; foo(); std::cout << global << std::endl; [] { ++global; } (); std::cout << global << std::endl; [global] { ++global; } (); } 

次のコードで遊ぶことができます: @Wandbox

自動ストレージの変数のみがキャプチャされます。 GCCは次の警告を発行することもあります。

 warning: capture of variable 'global' with non-automatic storage duration 

この警告は、グローバル変数を明示的にキャプチャした場合にのみ表示されるため、 [=]を使用した場合、コンパイラは役に立ちません。
Clangコンパイラはエラーを生成するため、より便利です。

 error: 'global' cannot be captured because it does not have automatic storage duration 

@Wandboxを参照

静的変数のキャプチャ

静的変数のキャプチャは、グローバルのキャプチャに似ています。

 #include <iostream> void bar() { static int static_int = 10; std::cout << static_int << std::endl; auto foo = [=] () mutable { ++static_int; }; foo(); std::cout << static_int << std::endl; [] { ++static_int; } (); std::cout << static_int << std::endl; [static_int] { ++static_int; } (); } int main() { bar(); } 

次のコードで遊ぶことができます: @Wandbox

結論:

 10 11 12 

繰り返しますが、静的変数を明示的にキャプチャした場合にのみ警告が表示されるため、 [=]を使用した場合、コンパイラは役に立ちません。

クラスメンバーキャプチャ

次のコードを実行した後に何が起こるか知っていますか:

 #include <iostream> #include <functional> struct Baz { std::function<void()> foo() { return [=] { std::cout << s << std::endl; }; } std::string s; }; int main() { auto f1 = Baz{"ala"}.foo(); auto f2 = Baz{"ula"}.foo(); f1(); f2(); } 

コードはBazオブジェクトを宣言してからfoo()呼び出します。 foo()は、クラスのメンバーをキャプチャするラムダ( std::function格納されているfoo()返すことに注意してください。

一時オブジェクトを使用しているため、f1とf2が呼び出されたときに何が起こるかはわかりません。 これは未定義のリンクの問題であり、未定義の動作を引き起こします。

同様に:

 struct Bar { std::string const& foo() const { return s; }; std::string s; }; auto&& f1 = Bar{"ala"}.foo(); //   

@Wandboxコードで遊ぶ

繰り返しますが、キャプチャを明示的に指定した場合([s]):

 std::function<void()> foo() { return [s] { std::cout << s << std::endl; }; } 

コンパイラはエラーを防ぎます:

 In member function 'std::function<void()> Baz::foo()': error: capture of non-variable 'Baz::s' error: 'this' was not captured for this lambda function ... 

例を参照してください: @Wandbox

移動のみ可能なオブジェクト

移動のみが可能なオブジェクト(unique_ptrなど)がある場合、キャプチャされた変数としてラムダに入れることはできません。 値によるキャプチャは機能しないため、参照によってのみキャプチャできます。ただし、これは転送されません。おそらく、これは望んでいないことです。

 std::unique_ptr<int> p(new int[10]); auto foo = [p] () {}; //  .... 

定数を保存する

定数変数をキャプチャする場合、不変性は保持されます。

 int const x = 10; auto foo = [x] () mutable { std::cout << std::is_const<decltype(x)>::value << std::endl; x = 11; }; foo(); 

コードを参照: @Wandbox

戻りタイプ

C ++ 11ではtrailing戻り値のラムダ型のtrailingをスキップできます。その後、コンパイラが出力します。

最初は、値の戻り型の出力は1つのreturnステートメントを含むラムダに制限されていましたが、より便利なバージョンの実装に問題はなかったため、この制限はすぐに削除されました。

C ++標準コア言語障害レポートおよび受け入れられた問題を参照してください(正しいリンクを見つけてくれたThomasに感謝します!)

したがって、C ++ 11以降では、すべてのreturnステートメントを同じ型に変換できる場合、コンパイラは戻り値の型を推測できます。
すべてのreturnステートメントが式を返し、左辺値から右辺値への変換(7.1 [conv.lval])、配列からポインター(7.2 [conv.array])、および関数からポインター(7.3 [conv。 func])はジェネリック型と同じです。
 auto baz = [] () { int x = 10; if ( x < 20) return x * 1.1; else return x * 2.1; }; 

次のコードで遊ぶことができます: @Wandbox

上記のラムダには2つのreturnがありますが、それらはすべてdouble指しているため、コンパイラーは型を推測できます。

IIFE-すぐに呼び出される関数式

この例では、ラムダを定義し、クロージャオブジェクトを使用してラムダを呼び出しました...しかし、すぐに呼び出すこともできます。

 int x = 1, y = 1; [&]() { ++x; ++y; }(); // <-- call () std::cout << x << " " << y << std::endl; 

このような式は、定数オブジェクトの複雑な初期化に役立ちます。

 const auto val = []() { /*   ... */ }(); 

これについては、 IIFEの複雑な初期化の投稿に書きました。

関数ポインターに変換する
キャプチャなしのラムダ式のクロージャー型には、定数をクロージャー型の関数を呼び出す演算子と同じパラメーターと戻り値の型を持つ関数へのポインターに変換する非仮想の暗黙的な関数があります。 この変換関数によって返される値は、関数のアドレスである必要があります。呼び出されると、クロージャー型に類似した型の関数の演算子を呼び出すのと同じ効果があります。
つまり、キャプチャなしのラムダを関数ポインターに変換できます。

例:

 #include <iostream> void callWith10(void(* bar)(int)) { bar(10); } int main() { struct { using f_ptr = void(*)(int); void operator()(int s) const { return call(s); } operator f_ptr() const { return &call; } private: static void call(int s) { std::cout << s << std::endl; }; } baz; callWith10(baz); callWith10([](int x) { std::cout << x << std::endl; }); } 

次のコードで遊ぶことができます: @Wandbox

C ++ 14の改善

N4140標準およびラムダ: [expr.prim.lambda]

C ++ 14では、ラムダ式に2つの重要な改善が追加されました。


これらの機能は、C ++ 11で見られたいくつかの問題を解決します。

戻りタイプ

ラムダ式の戻り値型の出力が更新され、関数の自動出力ルールに準拠するようになりました。

[expr.prim.lambda#4]
[dcl.spec.auto]で説明されているように、ラムダの戻り値の型はautoです。これは、returnステートメントから提供および/または推測される場合、末尾の戻り値の型に置き換えられます。
イニシャライザーでキャプチャ

つまり、クロージャー型の新しいメンバー変数を作成してから、ラムダ式内で使用できます。

例:

 int main() { int x = 10; int y = 11; auto foo = [z = x+y]() { std::cout << z << '\n'; }; foo(); } 

これにより、たとえば移動にのみ使用できるタイプの場合など、いくつかの問題を解決できます。

引越し

これで、オブジェクトをクロージャー型のメンバーに移動できます。

 #include <memory> int main() { std::unique_ptr<int> p(new int[10]); auto foo = [x=10] () mutable { ++x; }; auto bar = [ptr=std::move(p)] {}; auto baz = [p=std::move(p)] {}; } 

最適化

別のアイデアは、潜在的な最適化手法として使用することです。 ラムダを呼び出すたびに値を計算する代わりに、初期化子で1回計算できます。

 #include <iostream> #include <algorithm> #include <vector> #include <memory> #include <iostream> #include <string> int main() { using namespace std::string_literals; std::vector<std::string> vs; std::find_if(vs.begin(), vs.end(), [](std::string const& s) { return s == "foo"s + "bar"s; }); std::find_if(vs.begin(), vs.end(), [p="foo"s + "bar"s](std::string const& s) { return s == p; }); } 

メンバー変数をキャプチャする

初期化子を使用して、メンバー変数をキャプチャすることもできます。 その後、メンバー変数のコピーを取得でき、リンクのダングリングを心配する必要はありません。

例:

 struct Baz { auto foo() { return [s=s] { std::cout << s << std::endl; }; } std::string s; }; int main() { auto f1 = Baz{"ala"}.foo(); auto f2 = Baz{"ula"}.foo(); f1(); f2(); } 

次のコードで遊ぶことができます: @Wandbox


foo() 、クロージャー型にコピーしてメンバー変数をキャプチャします。 さらに、autoを使用してメソッド全体を出力します(以前は、C ++ 11ではstd::function使用できました)。

汎用ラムダ式

別の重要な改善点は、一般化されたラムダです。
C ++ 14以降では、次のように記述できます。

 auto foo = [](auto x) { std::cout << x << '\n'; }; foo(10); foo(10.1234); foo("hello world"); 

これは、クロージャータイプのcallステートメントでテンプレート宣言を使用することと同じです。

 struct { template<typename T> void operator()(T x) const { std::cout << x << '\n'; } } someInstance; 

このような一般化されたラムダは、型を推測することが難しい場合に非常に役立ちます。

例:

 std::map<std::string, int> numbers { { "one", 1 }, {"two", 2 }, { "three", 3 } }; //      pair<const string, int>! std::for_each(std::begin(numbers), std::end(numbers), [](const std::pair<std::string, int>& entry) { std::cout << entry.first << " = " << entry.second << '\n'; } ); 

私はここで間違っていますか? エントリのタイプは正しいですか?



std :: mapの値の型はstd::pair<const Key, T> 、おそらくそうではありません。 したがって、私のコードは行の追加コピーを作成します...
これはautoで修正できます:

 std::for_each(std::begin(numbers), std::end(numbers), [](auto& entry) { std::cout << entry.first << " = " << entry.second << '\n'; } ); 

次のコードで遊ぶことができます: @Wandbox

おわりに

なんて話だ!

この記事では、C ++ 03およびC ++ 11のラムダ式の最初の日から始め、C ++ 14の改良版に進みました。

ラムダの作成方法、この式の基本構造、キャプチャリストなどについて説明しました。

記事の次の部分では、C ++ 17に進み、C ++ 20の将来の機能について説明します。

2番目の部分は次の場所にあります。

ラムダ:C ++ 11からC ++ 20、パート2


参照資料

C ++ 11- [expr.prim.lambda]
C ++ 14-[expr.prim.lambda]
C ++でのラムダ式| マイクロソフトドキュメント
Demystifying C ++ lambdas-Sticky Bits-Powered by Feabhas; Sticky Bits-Powered by Feabhas


あなたのコメントをお待ちしており、コース「C ++ Developer」に興味のあるすべての人を招待します。

Source: https://habr.com/ru/post/J444524/


All Articles