web-dev-qa-db-ja.com

C++ 11のラムダ式とは何ですか?

C++ 11のラムダ式とは何ですか?いつ使うの?どのような種類の問題を解決するのですか。導入前は不可能でしたか。

いくつかの例とユースケースが役に立つでしょう。

1337
Nawaz

問題

C++にはstd::for_eachstd::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を1回しかその特定の場所で使用しないとしたら、ちょっとしたことと1つのことをやるためにクラス全体を書くのはやり過ぎです。

C++ 03では、Functorをローカルに保つために、次のようなものを書きたくなるかもしれません。

void func2(std::vector<int>& v) {
  struct {
    void operator()(int) {
       // do something
    }
  } f;
  std::for_each(v.begin(), v.end(), f);
}

ただし、これは許可されていません。fをC++ 03の template 関数に渡すことはできません。

新しい解決策

C++ 11では、struct fを置き換えるためのインラインの匿名の関数を書くことができるラムダが導入されました。小さな単純な例では、これは読みやすく(すべてを1か所にまとめておく)、潜在的に保守が簡単になります。例えば、最も単純な形式です。

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;
            }
        });
}

「キャプチャ」変数

これまでのところ、ラムダに渡されたもの以外のものは使用していませんが、ラムダ内で他の変数を使用することもできます。他の変数にアクセスしたい場合は、これまでのところこれらの例では使われていなかったcapture節(式の[])を使うことができます。例えば:

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になります。これは同じ入力を持つ各呼び出しが同じ結果を生成するという効果がありますが、生成されたoperator()mutableでないことを要求するために ラムダをconst としてマークすることができます。

1354
Flexo

ラムダ関数とは何ですか?

ラムダ関数のC++の概念は、ラムダ計算と関数型プログラミングに由来します。ラムダは名前の付いていない関数で、実際のプログラミングでは(理論上ではなく)再利用が不可能で命名に値しない短いコードの断片に役立ちます。

C++では、ラムダ関数は次のように定義されています。

[]() { } // barebone lambda

またはそのすべての栄光で

[]() mutable -> T { } // T is the return type, still lacking throw()

[]はキャプチャリスト、()は引数リスト、そして{}は関数本体です。

キャプチャリスト

キャプチャリストは、ラムダの外側から何を関数本体の中で利用できるようにするか、そしてその方法を定義します。どちらかになります。

  1. 値:[x]
  2. 参照[&x]
  3. 現在参照によって範囲内にある変数[&]
  4. 3と同じですが、値は[=]です。

上記のいずれかをコンマ区切りのリスト[x, &y]に混在させることができます。

引数リスト

引数リストは他のC++関数と同じです。

機能本体

ラムダが実際に呼び出されたときに実行されるコード。

戻り型の控除

ラムダにreturn文が1つしかない場合は、return型を省略して暗黙的なdecltype(return_statement)型にすることができます。

変わりやすい

ラムダが変更可能(例えば[]() mutable { })とマークされている場合、それは値によって捕らえられた値を変更することを許されます。

ユースケース

ISO規格で定義されているライブラリはラムダから大きな恩恵を受け、ユーザーがアクセス可能な範囲内で小さな関数を使ってコードを雑然とする必要がなくなるため、使いやすさが数バー向上します。

C++ 14

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.

ウィキペディアからstd::moveでキャプチャする方法を示すもの:

auto ptr = std::make_unique<int>(10); // See below for std::make_unique
auto lambda = [ptr = std::move(ptr)] {return *ptr;};

一般的なラムダ

ラムダは一般的になることができます(autoが周囲のスコープのどこかの型テンプレート引数である場合、ここでTTと同等になります)。

auto lambda = [](auto x, auto y) {return x + y;};

改善された戻り型の控除

C++ 14では、すべての関数について推定戻り型を使用でき、return expression;の形式の関数に限定されていません。これはラムダにも拡張されています。

789
pmr

ラムダ式は通常、アルゴリズムをカプセル化して別の関数に渡すことができるようにするために使用されます。ただし、 定義の直後にラムダを実行することは可能です

[&](){ ...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);

その後のプロファイリングで関数オブジェクトの初期化にかなりのオーバーヘッドがある場合は、これを通常の関数として書き換えることを選択できます。

162
nobar

回答

Q:C++ 11のラムダ式とは何ですか?

A:内部的には、それはオーバーロードoperator()constを持つ自動生成クラスのオブジェクトです。そのようなオブジェクトは クロージャ と呼ばれ、コンパイラによって作成されます。この「クロージャ」の概念は、C++ 11のバインドの概念に近いものです。しかし、ラムダは通常より良いコードを生成します。そしてクロージャを通しての呼び出しは完全なインライン展開を可能にします。

Q:いつ使うのですか?

A:「単純で小さな論理」を定義し、コンパイラに前の質問からの生成を依頼します。あなたはoperator()の中に入れたい式をコンパイラに与えます。他のすべてのものコンパイラはあなたに生成します。

Q:どのような種類の問題を解決するのですか。導入前は不可能でしたか。

A:カスタムの add、subrtact 操作のための関数ではなく、演算子のオーバーロードのようなある種の構文上の問題があります。 、 や。。など。!何人かのエンジニアは、行数が少なければそれを間違える可能性は少ないと思います(私もそう思います)

使用例

auto x = [=](int arg1){printf("%i", arg1); };
void(*f)(int) = x;
f(1);
x(1);

ラムダについての補足、質問でカバーされていません。興味がなければこのセクションを無視してください

1.取得値。あなたが捕獲することができるもの

1.1。あなたはラムダで静的な記憶期間で変数を参照することができます。それらはすべて捕らえられています。

1.2。値によるキャプチャ値にはラムダを使用できます。そのような場合、キャプチャされた変数は関数オブジェクトにコピーされます(クロージャ)。

[captureVar1,captureVar2](int arg1){}

1.3。あなたは参考になることができます。 & - これに関連して、ポインタではなく参照を意味します。

   [&captureVar1,&captureVar2](int arg1){}

1.4。値によって、または参照によってすべての非静的変数を取り込むための表記法が存在します。

  [=](int arg1){} // capture all not-static vars by value

  [&](int arg1){} // capture all not-static vars by reference

1.5。値によって、または参照によってすべての非静的変数を取り込んでsmthを指定するという表記法が存在します。もっと。例:静的ではないすべてのvarを値でキャプチャしますが、参照でキャプチャしますParam2

[=,&Param2](int arg1){} 

すべての非静的変数を参照によってキャプチャしますが、値キャプチャParam2によってキャプチャします。

[&,Param2](int arg1){} 

2.戻り型の控除

2.1。 lambdaが1つの式の場合、Lambdaの戻り値の型は推測できます。あるいは明示的に指定することもできます。

[=](int arg1)->trailing_return_type{return trailing_return_type();}

Lambdaに複数の式がある場合、戻り値の型は末尾の戻り値の型で指定する必要があります。同様の構文はauto関数とmember-functionsにも適用できます。

3.取得値。捉えられないもの

3.1。オブジェクトのメンバ変数ではなく、ローカル変数のみをキャプチャできます。

4.変換

4.1! Lambdaは関数ポインタではなく、無名関数でもありませんが、capture-less lambdasは暗黙的に関数ポインタに変換できます。

p.s.

  1. ラムダの文法情報についての詳細は、Programming Language for C++#337、2012-01-16、5.1.2にあります。ラムダ式、p.88

  2. C++ 14では、 "init capture"という名前の追加機能が追加されました。クロージャデータメンバの任意の宣言を実行することができます。

    auto toFloat = [](int value) { return float(value);};
    auto interpolate = [min = toFloat(0), max = toFloat(255)](int value)->float { return (value - min) / (max - min);};
    
38
bruziuz

ラムダ関数は、インラインで作成した無名関数です。説明したように変数をキャプチャできます(例: http://www.stroustrup.com/C++11FAQ.html#lambda )が、いくつかの制限があります。たとえば、次のようなコールバックインタフェースがあるとします。

void apply(void (*f)(int)) {
    f(10);
    f(20);
    f(30);
}

以下に適用するために渡されたもののようにそれを使用するためにその場で関数を書くことができます:

int col=0;
void output() {
    apply([](int data) {
        cout << data << ((++col % 10) ? ' ' : '\n');
    });
}

しかし、これはできません。

void output(int n) {
    int col=0;
    apply([&col,n](int data) {
        cout << data << ((++col % 10) ? ' ' : '\n');
    });
}

c ++ 11標準の制限のためです。キャプチャを使用したい場合は、ライブラリに頼る必要があります。

#include <functional> 

(またはそれを間接的に取得するアルゴリズムのような他のSTLライブラリ)次に、このように通常の関数をパラメータとして渡すのではなく、std :: functionを使用します。

#include <functional>
void apply(std::function<void(int)> f) {
    f(10);
    f(20);
    f(30);
}
void output(int width) {
    int col;
    apply([width,&col](int data) {
        cout << data << ((++col % width) ? ' ' : '\n');
    });
}
14
Ted

lambda expressionの最も良い説明の1つは、彼の本***The C++ Programming Language*** 11章( ISBN-13:978-0321563842 )のC++ Bjarne Stroustrup の作者から与えられます。

What is a lambda expression?

ラムダ式は、時にはラムダ関数または(厳密には間違っているが口語的に)ラムダとも呼ばれます。 無名関数オブジェクトを使用する 。名前付きクラスをoperator()で定義し、後でそのクラスのオブジェクトを作成し、最後にそれを呼び出す代わりに、省略形を使用できます。

When would I use one?

これは、アルゴリズムへの引数として操作を渡したいときに特に便利です。グラフィカルユーザインタフェース(および他の場所)の文脈では、そのような操作はしばしばコールバックと呼ばれます。

What class of problem do they solve that wasn't possible prior to their introduction?

ここで私はラムダ式で行われたすべてのアクションがそれらなしで解決されることができると思いますが、はるかに多くのコードとはるかに大きな複雑さで。ラムダ式これはあなたのコードを最適化する方法であり、より魅力的にする方法です。 Stroustupによって悲しいように:

最適化の効果的な方法

Some examples

ラムダ式による

void print_modulo(const vector<int>& v, ostream& os, int m) // output v[i] to os if v[i]%m==0
{
    for_each(begin(v),end(v),
        [&os,m](int x) { 
           if (x%m==0) os << x << '\n';
         });
}

または関数経由

class Modulo_print {
         ostream& os; // members to hold the capture list int m;
     public:
         Modulo_print(ostream& s, int mm) :os(s), m(mm) {} 
         void operator()(int x) const
           { 
             if (x%m==0) os << x << '\n'; 
           }
};

あるいは

void print_modulo(const vector<int>& v, ostream& os, int m) 
     // output v[i] to os if v[i]%m==0
{
    class Modulo_print {
        ostream& os; // members to hold the capture list
        int m; 
        public:
           Modulo_print (ostream& s, int mm) :os(s), m(mm) {}
           void operator()(int x) const
           { 
               if (x%m==0) os << x << '\n';
           }
     };
     for_each(begin(v),end(v),Modulo_print{os,m}); 
}

必要ならば、以下のようにlambda expressionを指定できます。

void print_modulo(const vector<int>& v, ostream& os, int m)
    // output v[i] to os if v[i]%m==0
{
      auto Modulo_print = [&os,m] (int x) { if (x%m==0) os << x << '\n'; };
      for_each(begin(v),end(v),Modulo_print);
 }

または別の簡単なサンプルを仮定する

void TestFunctions::simpleLambda() {
    bool sensitive = true;
    std::vector<int> v = std::vector<int>({1,33,3,4,5,6,7});

    sort(v.begin(),v.end(),
         [sensitive](int x, int y) {
             printf("\n%i\n",  x < y);
             return sensitive ? x < y : abs(x) < abs(y);
         });


    printf("sorted");
    for_each(v.begin(), v.end(),
             [](int x) {
                 printf("x - %i;", x);
             }
             );
}

次に生成します

1

1

1

1

1

0ソート済みx - 1; x - 3; x - 4; x - 5; x - 6; x - 7; x - 33;

[] - これはキャプチャリストまたはlambda introducerです。lambdasがローカル環境へのアクセスを必要としない場合は、それを使用できます。

本からの引用:

ラムダ式の最初の文字は常に [ です。ラムダイントロデューサはさまざまな形式を取ります。

[] :空のキャプチャリスト。これは、周囲のコンテキストからのローカル名をラムダ本体で使用できないことを意味します。このようなラムダ式の場合、データは引数または非局所変数から取得されます。

[&] :暗黙的に参照によるキャプチャ。すべてのローカル名を使用できます。すべてのローカル変数は参照によってアクセスされます。

[=] :暗黙的に値でキャプチャします。すべてのローカル名を使用できます。すべての名前は、ラムダ式の呼び出し時点で取得されたローカル変数のコピーを表します。

[capture-list]: 明示的なキャプチャ。キャプチャリストは、参照によって、または値によってキャプチャされる(すなわち、オブジェクトに格納される)ローカル変数の名前のリストである。名前の前に&が付いた変数は、参照によって取り込まれます。他の変数は値によって取り込まれます。キャプチャリストには、thisと名前の後に...を要素として含めることもできます。

[&、capture-list] :リストに記載されていない名前を持つすべてのローカル変数を参照によって暗黙的にキャプチャします。キャプチャリストにこれを含めることができます。リストされた名前の前に&を付けることはできません。キャプチャリストで指定された変数は値によってキャプチャされます。

[=、capture-list] :リストに記載されていない名前を持つすべてのローカル変数を暗黙的に値でキャプチャします。キャプチャリストにこれを含めることはできません。リストされた名前の前に&を付けなければなりません。キャプチャリストで指定された変数は、参照によってキャプチャされます。

先頭に&が付いたローカル名は常に参照によってキャプチャーされ、先頭に&が付いていないローカル名は常に値によってキャプチャーされます。参照によるキャプチャのみで、呼び出し元の環境で変数を変更できます。

Additional

Lambda expressionの形式

enter image description here

その他の参考文献

10
gbk

実は、私が発見した実用的な使用法の1つは、ボイラープレートコードを減らすことです。例えば:

void process_z_vec(vector<int>& vec)
{
  auto print_2d = [](const vector<int>& board, int bsize)
  {
    for(int i = 0; i<bsize; i++)
    {
      for(int j=0; j<bsize; j++)
      {
        cout << board[bsize*i+j] << " ";
      }
      cout << "\n";
    }
  };
  // Do sth with the vec.
  print_2d(vec,x_size);
  // Do sth else with the vec.
  print_2d(vec,y_size);
  //... 
}

ラムダがなければ、さまざまなbsizeの場合に何かする必要があるかもしれません。もちろん、関数を作成することもできますが、その使用をsoulユーザー関数の範囲内に制限したい場合はどうしますか?ラムダの性質がこの要件を満たしているので、その場合に使用します。

2
Misgevolution

それが解決する1つの問題: constメンバを初期化するために出力パラメータ関数を使用するコンストラクタでの呼び出しのためのラムダより単純なコード

自分のクラスのconstメンバーを初期化することができます。その出力を出力パラメータとして返すことによってその値を設定する関数を呼び出します。

1
sergiol

C++のラムダは、 "実行中の関数では"として扱われます。はい文字通り外出先で、あなたはそれを定義します。これを使って;そして、親関数のスコープが終了すると、ラムダ関数はなくなります。

c ++はc ++ 11でそれを導入しました、そして、誰もがあらゆる可能な場所のようにそれを使い始めました。例とラムダとはここにあります https://en.cppreference.com/w/cpp/language/lambda

私はどれがそこにあるのではなく、すべてのc ++プログラマーのために知ることが不可欠であるかについて説明する

ラムダは至る所で使用することを意味しておらず、そしてすべての機能をラムダで置き換えることはできません。通常の機能と比較しても最速ではありません。なぜならそれはラムダによって処理される必要があるいくらかのオーバーヘッドがあるからです。

それは確かにいくつかのケースで行数を減らすのに役立ちます。基本的には同じ関数内で1回以上呼び出されるコードのセクションに使用でき、そのコードは他の場所では必要ないため、スタンドアロン関数を作成できます。

以下は、ラムダの基本的な例と背景で起こることです。

ユーザーコード:

int main()
{
  // Lambda & auto
  int member=10;
  auto endGame = [=](int a, int b){ return a+b+member;};

  endGame(4,5);

  return 0;

}

どのようにコンパイルして展開しますか:

int main()
{
  int member = 10;

  class __lambda_6_18
  {
    int member;
    public: 
    inline /*constexpr */ int operator()(int a, int b) const
    {
      return a + b + member;
    }

    public: __lambda_6_18(int _member)
    : member{_member}
    {}

  };

  __lambda_6_18 endGame = __lambda_6_18{member};
  endGame.operator()(4, 5);

  return 0;
}

ご覧のとおり、使用するとどのようなオーバーヘッドが追加されるのでしょうか。だからどこでもそれらを使うのは良い考えではありません。適用できる場所で使用できます。

0
Sachin Nale