C ++ 11のラムダで、値によるキャプチャにデフォルトで「可変」キーワードが必要なのはなぜですか?


256

短い例:

#include <iostream>

int main()
{
    int n;
    [&](){n = 10;}();             // OK
    [=]() mutable {n = 20;}();    // OK
    // [=](){n = 10;}();          // Error: a by-value capture cannot be modified in a non-mutable lambda
    std::cout << n << "\n";       // "10"
}

質問:なぜmutableキーワードが必要なのですか?名前付き関数に渡す従来のパラメーターとはかなり異なります。背後にある理論的根拠は何ですか?

値によるキャプチャの全体のポイントは、ユーザーが一時変数を変更できるようにすることだという印象を受けました。それ以外の場合は、ほとんどの場合、参照によるキャプチャを使用した方がよいのではないでしょうか。

悟りはありますか?

(ちなみに私はMSVC2010を使用しています。これは標準のはずです)


101
良い質問; constデフォルトで何かがついに嬉しいですが!
xtofl 2011年

3
答えではありませんが、これは賢明なことだと思います。値によって何かを取得する場合、1つのコピーをローカル変数に保存するためだけに変更するべきではありません。少なくとも、=を&に置き換えてnを変更するミスはありません。
stefaanv 2011年

8
@xtofl:他のすべてがconstデフォルトではない場合、それが良いかどうかはわかりません。
kizzx2

8
@TamásSzelei:議論を始めるのではなく、IMHOの「簡単に習得できる」という概念は、特に現代ではC ++言語では意味がありません。とにかく:P
kizzx2

3
「値によるキャプチャの全体的なポイントは、ユーザーが一時変数を変更できるようにすることです」-いいえ、全体的なポイントは、ラムダはキャプチャされた変数の存続期間を超えて有効なままである可​​能性があるということです。C ++ラムダに参照によるキャプチャしかない場合、それらはあまりにも多くのシナリオで使用できなくなります。
Sebastian Redl

回答:


230

mutableデフォルトでは、関数オブジェクトは呼び出されるたびに同じ結果を生成する必要があるためです。これは、オブジェクト指向関数とグローバル変数を効果的に使用する関数の違いです。


7
これは良い点です。全くもって同じ意見です。ただし、C ++ 0xでは、デフォルトが上記の強制にどのように役立つかはよくわかりません。私がラムダの受信側にいるとしますvoid f(const std::function<int(int)> g)。それgが実際に参照的に透過的であることはどのように保証されますか?gのサプライヤーがmutableとにかく使用した可能性があります。だから私は知りません。一方、デフォルトがnon- constであり、人々が関数オブジェクトのconst代わりに追加しなければならないmutable場合、コンパイラーは実際にそのconst std::function<int(int)>部分を強制fすることgができ、今ではそうだと想定できconstますか?
kizzx2

8
@ kizzx2:C ++では何も強制されず、推奨されるだけです。いつものように、あなたが何か愚かなことをした場合(参照の透明性に関する文書化された要件と、それから非参照的に透明な関数を渡す)、あなたは何でもあなたにやって来ます。
パピー

6
この答えは私の目を開きました。以前は、この場合ラムダは現在の「実行」のコピーのみを変更すると思っていました。
Zsolt Szatmari 2015年

4
@ZsoltSzatmariあなたのコメントは私の目を開いた!:-DIはあなたのコメントを読むまでこの答えの真の意味を理解していませんでした。
Jendas

5
この回答の基本的な前提に同意しません。C ++には、言語の他の場所で「関数は常に同じ値を返す」という概念はありません。設計原則として、関数を記述するのは良い方法だと思いますが、標準的な動作の理由としてそれが水を保持しているとは思いません。
イオノクラストブリガム2017

103

あなたのコードはこれとほぼ同等です:

#include <iostream>

class unnamed1
{
    int& n;
public:
    unnamed1(int& N) : n(N) {}

    /* OK. Your this is const but you don't modify the "n" reference,
    but the value pointed by it. You wouldn't be able to modify a reference
    anyway even if your operator() was mutable. When you assign a reference
    it will always point to the same var.
    */
    void operator()() const {n = 10;}
};

class unnamed2
{
    int n;
public:
    unnamed2(int N) : n(N) {}

    /* OK. Your this pointer is not const (since your operator() is "mutable" instead of const).
    So you can modify the "n" member. */
    void operator()() {n = 20;}
};

class unnamed3
{
    int n;
public:
    unnamed3(int N) : n(N) {}

    /* BAD. Your this is const so you can't modify the "n" member. */
    void operator()() const {n = 10;}
};

int main()
{
    int n;
    unnamed1 u1(n); u1();    // OK
    unnamed2 u2(n); u2();    // OK
    //unnamed3 u3(n); u3();  // Error
    std::cout << n << "\n";  // "10"
}

したがって、ラムダは、可変であると言わない限り、デフォルトでconstになるoperator()でクラスを生成すると考えることができます。

[]内で(明示的または暗黙的に)キャプチャされたすべての変数を、そのクラスのメンバーとして考えることもできます。[=]のオブジェクトのコピーまたは[&]のオブジェクトへの参照。隠しコンストラクタがあるかのようにラムダを宣言すると、これらは初期化されます。


5
素敵な説明をしながら、constmutable同等のユーザー定義型として実装されている場合、質問は(タイトルのように、コメントにOPで詳述)であるように見えるラムダなぜ constデフォルトであるので、これはそれに答えていません。
underscore_d

36

値によるキャプチャの全体のポイントは、ユーザーが一時変数を変更できるようにすることだという印象を受けました。それ以外の場合は、ほとんどの場合、参照によるキャプチャを使用した方がよいのではないでしょうか。

問題は、「ほとんど」ですか。よく使用されるのは、ラムダを返すか渡すことです。

void registerCallback(std::function<void()> f) { /* ... */ }

void doSomething() {
  std::string name = receiveName();
  registerCallback([name]{ /* do something with name */ });
}

それmutableは「ほとんど」の場合ではないと思います。「値のキャプチャ」は、「値のコピーを変更できるようにする」ではなく、「値がキャプチャされたエンティティが終了した後でその値を使用できるようにする」などと考えています。しかし、おそらくこれは議論の余地があります。


2
良い例え。これは、値によるキャプチャを使用する非常に強力なユースケースです。しかし、なぜそれがデフォルトになるのconstですか?それはどのような目的を達成しますか?「ほとんど」(:P)のデフォルトではないmutable場合、言語のその他すべてのものはここでは場違いのようです。const
kizzx2 2011年

8
@ kizzx2:私constがデフォルトだったらいいのに、少なくとも人々はconst-correctnessを検討せざるを得ないでしょう:/
Matthieu M.

1
@ kizzx2はラムダペーパーを調べてconst、ラムダオブジェクトがconstであるかどうかに関係なく呼び出せるように、デフォルトに設定しているように見えます。たとえば、それはを受け取る関数にそれを渡すことができstd::function<void()> const&ます。ラムダがキャプチャしたコピーを変更できるようにするために、最初の論文では、クロージャーのデータメンバーがmutable内部で自動的に定義されていました。次にmutable、ラムダ式を手動で入力する必要があります。詳細な根拠は見つかりませんでした。
Johannes Schaub-litb '31年


5
この時点で、私には、「実際の」答え/根拠は「実装の詳細を回避できなかった」ようです:/
kizzx2

32

C ++標準化委員会の有名なメンバーであるFWIW、Herb Sutterは、Lambdaの正確性と使いやすさの問題で、その質問に対する別の回答を提供しています。

プログラマーがローカル変数を値でキャプチャーし、キャプチャーした値(ラムダオブジェクトのメンバー変数)を変更しようとする、このストローマンの例を考えてみましょう。

int val = 0;
auto x = [=](item e)            // look ma, [=] means explicit copy
            { use(e,++val); };  // error: count is const, need ‘mutable’
auto y = [val](item e)          // darnit, I really can’t get more explicit
            { use(e,++val); };  // same error: count is const, need ‘mutable’

この機能は、ユーザーがコピーを取得したことに気付かない可能性があること、特にラムダはコピー可能であるため、別のラムダのコピーを変更する可能性があるという懸念から追加されたようです。

彼の論文は、これがC ++ 14で変更されるべき理由についてです。この特定の機能に関して「[委員会メンバー]の考え方」を知りたい場合は、短く、よく書かれており、読む価値があります。


16

Lambda関数のクロージャタイプは何かを考える必要があります。Lambda式を宣言するたびに、コンパイラーはクロージャー型を作成します。これは、属性(Lambda式が宣言されている環境)と関数呼び出しが::operator()実装された名前のないクラス宣言にほかなりません。copy-by-valueを使用して変数をキャプチャすると、コンパイラはconstクロージャタイプに新しい属性を作成します。これは、「読み取り専用」属性であるため、Lambda式内で変更できないためです。これを「クロージャ」と呼びます。何らかの方法で、変数を上位スコープからLambdaスコープにコピーしてLambda式を閉じるため、」ます。mutable、キャプチャされたエンティティはなりnon-const閉鎖タイプの属性。これにより、値によってキャプチャされた可変変数で行われた変更が上位スコープに伝播されず、ステートフルなLambda内に保持されます。Lambda式の結果として生じるクロージャタイプを常に想像してみてください。それが私を大きく助けました。それがあなたにも役立つことを願っています。


14

5.1.2 [expr.prim.lambda]の5節の次のドラフトを参照してください。

ラムダ式のクロージャ型には、パブリックインライン関数呼び出し演算子(13.5.4)があり、そのパラメータと戻り値の型は、ラムダ式のparameter-declaration-clauseとtrailingreturn-typeによってそれぞれ記述されます。この関数呼び出し演算子は、ラムダ式のparameter-declaration-clauseの後にミュータブルが続かない場合にのみconst(9.3.1)と宣言されます。

litbのコメントを編集:変数への外部の変更がラムダの内部に反映されないように、値によるキャプチャを考えたのでしょうか?参照は両方の方法で機能するので、それが私の説明です。それが良いかどうか分からない。

kizzx2のコメントを編集:ラムダが使用されるほとんどの場合は、アルゴリズムのファンクタとして使用されます。デフォルトの状態constでは、通常のconst修飾された関数はそこで使用できますが、const修飾されていない関数は使用できないように、定数環境で使用できます。多分彼らは彼らの心の中で何が起こっているかを知っているそれらの場合のためにそれをより直感的にすることを考えただけかもしれません。:)


それは標準ですが、なぜ彼らはそれをこのように書いたのですか?
kizzx2

@ kizzx2:私の説明はその引用のすぐ下にあります。:)これは、キャプチャされたオブジェクトの寿命についてlitbが言っていることと少し関係がありますが、少し先に進んでください。
Xeo

@Xeo:ああそうです、それを逃しました:Pこれは、値によるキャプチャを上手に使用するためのもう1つの良い説明でもあります。しかし、なぜそれがconstデフォルトであるべきなのでしょうか?私はすでに新しいコピーを手に入れました、私にそれを変更させないのは奇妙に思えます-特にそれは本質的にそれで問題はありません-彼らは私に追加して欲しいだけですmutable
kizzx2

名前付きラムダのように見える、新しい総称関数宣言構文を作成する試みがあったと思います。また、デフォルトですべてをconstにすることで、他の問題を修正することも想定されていました。決して完成しませんでしたが、アイデアはラムダの定義にこすりつけられました。
Bo Persson、2011年

2
@ kizzx2-最初からやり直すことができればvar、変更を許可するためのキーワードとして、定数を他のすべてのデフォルトにすることができます。今はそうではないので、私たちはそれと共存しなければなりません。IMO、C ++ 2011はすべてを考慮してかなりうまくいきました。
Bo Persson

11

値によるキャプチャの全体のポイントは、ユーザーが一時変数を変更できるようにすることだという印象を受けました。それ以外の場合は、ほとんどの場合、参照によるキャプチャを使用した方がよいのではないでしょうか。

n一時的なものではありません。nは、ラムダ式を使用して作成するラムダ関数オブジェクトのメンバーです。デフォルトの想定では、ラムダを呼び出してもその状態は変更されないため、誤ってを変更しないようにする必要がありますn


1
ラムダオブジェクト全体は一時的なものであり、そのメンバーにも一時的な有効期間があります。
Ben Voigt 2014年

2
@Ben:IIRC、私が誰かが「一時的」と言うとき、私はそれがラムダ自体がそうである名前のない一時的オブジェクトを意味すると理解するという問題に言及していましたが、そのメンバーはそうではありません。また、ラムダの「内部」からは、ラムダ自体が一時的なものであるかどうかは問題ではありません。OPが「一時的」と言ったとき、OPは単に「ラムダの内側のn」を言うことを意図していたように見えますが、質問をもう一度読んでください。
Martin Ba

6

キャプチャの意味を理解する必要があります!引数の受け渡しではなくキャプチャしています!いくつかのコードサンプルを見てみましょう。

int main()
{
    using namespace std;
    int x = 5;
    int y;
    auto lamb = [x]() {return x + 5; };

    y= lamb();
    cout << y<<","<< x << endl; //outputs 10,5
    x = 20;
    y = lamb();
    cout << y << "," << x << endl; //output 10,20

}

ご覧のとおり、ラムダにx変更されても、2010が返されます(xまだ5ラムダx内にあります)。ラムダ内を変更すると、各呼び出しでラムダ自体が変更されます(ラムダは各呼び出しで変化します)。正確さを強制するために、標準はmutableキーワードを導入しました。ラムダを可変として指定することにより、ラムダへの各呼び出しがラムダ自体に変更を引き起こす可能性があることを意味します。別の例を見てみましょう:

int main()
{
    using namespace std;
    int x = 5;
    int y;
    auto lamb = [x]() mutable {return x++ + 5; };

    y= lamb();
    cout << y<<","<< x << endl; //outputs 10,5
    x = 20;
    y = lamb();
    cout << y << "," << x << endl; //outputs 11,20

}

上記の例は、ラムダを可変にしxて、ラムダ内を変更するたびに、呼び出しごとに新しい値でラムダを「変更」し、メイン関数のx実際の値とは関係がないことを示していxます。


4

現在、mutableラムダ宣言の必要性を緩和する提案があります:n3424


それから何が起こったのかについての情報はありますか?新しい「任意の表現のキャプチャ」は、ほとんどの問題点を解消するので、個人的には悪い考えだと思います。
Ben Voigt 2013年

1
@BenVoigtええ、それは変更のための変更のようです。
Miles Rout 2014年

3
@BenVoigt公平ではありますがmutable、C ++のキーワードでさえ知らないC ++開発者が多分いると思います。
Miles Rout 2014年

1

パピーの答えを拡張するために、ラムダ関数は純粋な関数であることが意図されています。つまり、一意の入力セットが指定されたすべての呼び出しは、常に同じ出力を返します。ラムダが呼び出されたときに、すべての引数とキャプチャされたすべての変数のセットとして入力を定義しましょう。

純粋な関数では、出力は一部の内部状態ではなく入力のみに依存します。したがって、ラムダ関数は、純粋であれば、その状態を変更する必要がないため、不変です。

ラムダが参照によってキャプチャする場合、キャプチャされた変数への書き込みは、純粋な関数の概念に対する負担になります。これは、純粋な関数が行うべきことはすべて出力を返すことです。この場合でも正しい使用法は、参照による変数へのこれらの副作用にもかかわらず、ラムダが同じ入力で再度呼び出された場合、出力は毎回同じになることを意味します。このような副作用は、追加の入力(カウンターの更新など)を返すための単なる方法であり、単一の値の代わりにタプルを返すなど、純粋な関数に再構成できます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.