こんにちは、友達。 本日、記事
「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'; } ); }
例:
@WandBoxCppInsightsも確認できます。これは、コンパイラがコードをどのように拡張するかを示しています。
この例を見てください:
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; }
定数メソッドは、空のキャプチャリストのないラムダの「問題」ではありませんが、何かをキャプチャしたいときに重要です。
キャプチャー[]はラムダを導入するだけでなく、キャプチャされた変数のリストも含みます。 これはキャプチャリストと呼ばれます。
変数をキャプチャすることにより、この変数のコピーメンバーをクロージャータイプで作成します。 次に、ラムダ本体内でアクセスできます。
基本的な構文は次のとおりです。
- [&]-参照によるキャプチャ。自動ストレージ内のすべての変数はスコープ内で宣言されます
- [=]-値でキャプチャ、値がコピーされます
- [x、&y]-値によってxを明示的にキャプチャし、参照によってyをキャプチャします
例:
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; }();
このような式は、定数オブジェクトの複雑な初期化に役立ちます。
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; }); }
次のコードで遊ぶことができます:
@WandboxC ++ 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 } };
私はここで間違っていますか? エントリのタイプは正しいですか?
。
。
。
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」に興味のあるすべての人を招待します。