web-dev-qa-db-ja.com

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

短い例:

#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を使用しています。これは標準的なはずです)

238
kizzx2

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

217
Puppy

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

#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()でクラスを生成すると考えることができます。

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

99
Daniel Munoz

値によるキャプチャのすべてのポイントは、ユーザーが一時的な値を変更できるようにすることであるという印象を受けていました。

問題は、「ほぼ」ですか?頻繁に使用されるケースは、ラムダを返すか渡すことです。

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

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

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

FWIW、C++標準化委員会の有名なメンバーであるハーブサッターは、その質問に対する別の答えを ラムダの正確さと使いやすさの問題 :で提供しています。

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

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で変更すべきかについてです。この特定の機能に関して「[委員会メンバー]の心に何があるか」を知りたい場合は、短く、よく書かれており、読む価値があります。

27
akim

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

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

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

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

15
Xeo

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

12
Tarantula

値によるキャプチャのすべてのポイントは、ユーザーが一時的な値を変更できるようにすることであるという印象を受けていました。

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

10
Martin Ba

ラムダ宣言でmutableの必要性を軽減する提案があります: n3424

4
usta

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

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

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

0
Attersson