簡體   English   中英

為什么 C++11 的 lambda 默認情況下需要“可變”關鍵字用於按值捕獲?

[英]Why does C++11's lambda require "mutable" keyword for capture-by-value, by default?

簡短的例子:

#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。AFAIK 這應該是標准的)

它需要mutable因為默認情況下,函數對象每次被調用時都應該產生相同的結果。 這是面向對象的函數和有效地使用全局變量的函數之間的區別。

您的代碼幾乎等同於:

#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"
}

因此,您可以將 lambda 視為生成一個帶有 operator() 的類,該類默認為 const,除非您說它是可變的。

您還可以將 [] 中捕獲的所有變量(顯式或隱式)視為該類的成員:[=] 的對象副本或對 [&] 對象的引用。 當您聲明 lambda 時,它們會被初始化,就好像有一個隱藏的構造函數一樣。

我的印象是,按值捕獲的全部意義在於允許用戶更改臨時值——否則我幾乎總是最好使用按引用捕獲,不是嗎?

問題是,是“幾乎”嗎? 一個常見的用例似乎是返回或傳遞 lambdas:

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

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

我認為mutable不是“幾乎”的情況。 我認為“按值捕獲”類似於“允許我在捕獲的實體死亡后使用其值”而不是“允許我更改它的副本”。 但也許這是可以爭論的。

FWIW,C++ 標准化委員會的知名成員 Herb Sutter 在Lambda Correctness and Usability Issues 中對這個問題提供了不同的答案:

考慮這個稻草人示例,其中程序員按值捕獲局部變量並嘗試修改捕獲的值(這是 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'

添加此功能似乎是出於擔心用戶可能沒有意識到他獲得了副本,特別是由於 lambda 是可復制的,因此他可能正在更改不同的 lambda 副本。

他的論文是關於為什么應該在 C++14 中改變這一點。 如果您想了解有關此特定功能的“[委員會成員] 的想法”,那么它很短,寫得很好,值得一讀。

您必須了解捕獲的含義! 它捕獲而不是參數傳遞! 讓我們看一些代碼示例:

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已更改為20 ,lambda 仍返回 10( x在 lambda 內仍為5在 lambda 內更改x意味着在每次調用時更改 lambda 本身(lambda 在每次調用時都會發生變化)。 為了強制執行正確性,標准引入了mutable關鍵字。 通過將 lambda 指定為可變的,您是說對 lambda 的每次調用都可能導致 lambda 本身發生變化。 讓我們看另一個例子:

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

}

上面的例子表明,通過使 lambda 可變,改變 lambda 內的x ,在每次調用時使用與主函數中x的實際值無關的新值x來“變異” lambda

您需要考慮 Lambda 函數的閉包類型是什么。 每次聲明一個 Lambda 表達式時,編譯器都會創建一個閉包類型,它只不過是一個帶有屬性(聲明 Lambda 表達式的環境)和函數調用::operator()實現的未命名類聲明。 當您使用copy-by-value捕獲變量時,編譯器將在閉包類型中創建一個新的const屬性,因此您無法在 Lambda 表達式中更改它,因為它是“只讀”屬性,這就是原因他們稱之為“閉包”,因為在某種程度上,您通過將變量從上層范圍復制到 Lambda 范圍來關閉您的 Lambda 表達式。 當您使用關鍵字mutable ,捕獲的實體將成為您的閉包類型的non-const屬性。 這就是導致在由值捕獲的可變變量中所做的更改不會傳播到上層作用域,而是保留在有狀態的 Lambda 中的原因。 總是試着想象你的 Lambda 表達式的結果閉包類型,這對我有很大幫助,我希望它也能幫助你。

請參閱此草案,在 5.1.2 [expr.prim.lambda] 下的第 5 款:

lambda 表達式的閉包類型有一個公共內聯函數調用運算符 (13.5.4),其參數和返回類型分別由 lambda 表達式的參數聲明子句和尾隨返回類型描述。 此函數調用運算符聲明為 const (9.3.1) 當且僅當 lambda 表達式的參數聲明子句后面沒有可變的。

編輯 litb 的評論:也許他們想到了按值捕獲,以便變量的外部更改不會反映在 lambda 內部? 參考文獻是雙向的,所以這就是我的解釋。 不過不知道好不好用。

編輯 kizzx2 的評論:使用 lambda 的最多次數是作為算法的函子。 默認的const ness 允許它在常量環境中使用,就像可以在那里使用普通的const限定函數一樣,但非const限定的函數不能。 也許他們只是想讓這些案例更直觀,誰知道他們在想什么。 :)

我的印象是,按值捕獲的全部意義在於允許用戶更改臨時值——否則我幾乎總是最好使用按引用捕獲,不是嗎?

n不是臨時的。 n 是您使用 lambda 表達式創建的 lambda 函數對象的成員。 默認期望是調用 lambda 不會修改其狀態,因此 const 可以防止您意外修改n

現在有一個建議可以減輕 lambda 聲明中對mutable的需求: n3424

為了擴展 Puppy 的答案,lambda 函數旨在成為純函數 這意味着給定唯一輸入集的每個調用總是返回相同的輸出。 讓我們將輸入定義為調用 lambda 時所有參數加上所有捕獲變量的集合。

在純函數中,輸出僅取決於輸入而不取決於某些內部狀態。 因此,任何 lambda 函數,如果是純函數,都不需要改變其狀態,因此是不可變的。

當 lambda 通過引用捕獲時,寫入捕獲的變量是對純函數概念的一種壓力,因為純函數應該做的就是返回一個輸出,盡管 lambda 肯定不會發生變化,因為寫入發生在外部變量上。 即使在這種情況下,正確的用法也意味着如果再次使用相同的輸入調用 lambda,輸出將每次都相同,盡管對 by-ref 變量有這些副作用。 這種副作用只是返回一些額外輸入(例如更新計數器)的方法,可以重新表述為純函數,例如返回元組而不是單個值。

我也想知道為什么[=]<\/code>需要顯式mutable<\/code>的最簡單的解釋是在這個例子中:

int main()
{
    int x {1};
    auto lbd = [=]() mutable { return x += 5; };
    printf("call1:%d\n", lbd());
    printf("call2:%d\n", lbd());
    return 0;
}

如果您檢查 lambda 的 3 個不同用例,您可能會看到差異:

  • 按值捕獲參數
  • 使用“可變”關鍵字按值捕獲參數
  • 通過引用捕獲參數

案例 1:當您按值捕獲參數時,會發生一些事情:

  • 不允許修改 lambda 中的參數
  • 每當調用 lambda 時,參數的值保持不變,無論調用 lambda 時的參數值是什么。

例如:

{
    int x = 100;
    auto lambda1 = [x](){
      // x += 2;  // compile time error. not allowed
                  // to modify an argument that is captured by value
      return x * 2;
    };
    cout << lambda1() << endl;  // 100 * 2 = 200
    cout << "x: " << x << endl; // 100

    x = 300;
    cout << lambda1() << endl;   // in the lambda, x remain 100. 100 * 2 = 200
    cout << "x: " << x << endl;  // 300

}

Output: 
200
x: 100
200
x: 300

案例 2:在這里,當您按值捕獲參數並使用“可變”關鍵字時,與第一種情況類似,您創建此參數的“副本”。 這個“副本”存在於 lambda 的“世界”中,但是現在,您實際上可以在 lambda-world 中修改參數,因此它的值被更改,並保存並可以引用,在未來的調用中lambda。 同樣,論點的外部“生命”可能完全不同(價值明智):

{
    int x = 100;
    auto lambda2 = [x]() mutable {
      x += 2;  // when capture by value, modify the argument is
               // allowed when mutable is used.
      return x;
    };
    cout << lambda2() << endl;  // 100 + 2 = 102
    cout << "x: " << x << endl; // in the outside world - x remains 100
    x = 200;
    cout << lambda2() << endl;  // 104, as the 102 is saved in the lambda world.
    cout << "x: " << x << endl; // 200
}

Output:
102
x: 100
104
x: 200

情況 3:這是最簡單的情況,因為 x 沒有 2 條生命。 現在 x 只有一個值,它在外部世界和 lambda 世界之間共享。

{
    int x = 100;
    auto lambda3 = [&x]() mutable {
        x += 10;  // modify the argument, is allowed when mutable is used.
        return x;
    };
    cout << lambda3() << endl;  // 110
    cout << "x: " << x << endl; // 110
    x = 400;
    cout << lambda3() << endl;  // 410.
    cout << "x: " << x << endl; // 410
}

Output: 
110
x: 110
410
x: 410

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM