为什么C ++ 11的lambda默认需要“可变”关键字用于按值捕获?

时间:2011-03-31 15:02:52

标签: c++ lambda 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关键字?它与传统参数传递给命名函数有很大不同。背后的理由是什么?

我的印象是,按值捕获的全部要点是允许用户更改临时值 - 否则我几乎总是更好地使用按引用捕获,不是吗?< / p>

任何启示?

(我正在使用MSVC2010.AFAIK这应该是标准的)

10 个答案:

答案 0 :(得分:212)

它需要mutable,因为默认情况下,每次调用时,函数对象都应该生成相同的结果。这是面向对象的函数和使用全局变量的函数之间的区别。

答案 1 :(得分:96)

您的代码几乎与此相同:

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

所以你可以认为lambdas生成一个带有operator()的类,默认为const,除非你说它是可变的。

您还可以将[](显式或隐式)内捕获的所有变量视为该类的成员:[=]的对象副本或[&amp;]对象的引用。当你将lambda声明为隐藏的构造函数时,它们会被初始化。

答案 2 :(得分:35)

  

我的印象是,按值捕获的全部要点是允许用户更改临时值 - 否则我几乎总是更好地使用按引用捕获,不是吗?< / p>

问题是,它“差不多”了吗?一个常见的用例似乎是返回或传递lambdas:

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

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

我认为mutable不是“差不多”的情况。我认为“按值捕获”就像“允许我在捕获的实体死后使用它的值”而不是“允许我更改它的副本”。但也许这可以争论。

答案 3 :(得分:26)

FWIW,Herb Sutter,C ++标准化委员会的知名成员,在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’
     

此功能似乎是出于对用户的关注而添加的   可能没有意识到他得到了副本,特别是因为lambdas   可复制的他可能正在改变另一个lambda的副本。

他的论文是关于为什么要在C ++ 14中改变它。它简短,写得很好,如果你想知道“关于这个特殊功能的[委员会成员]的想法”,那就值得一读。

答案 4 :(得分:14)

参见this draft,在5.1.2 [expr.prim.lambda]下,第5节:

  

lambda-expression的闭包类型有一个公共内联函数调用操作符(13.5.4),其参数   和return类型由lambda-expression的parameter-declaration-clause和trailingreturn-描述 -   分别是类型。 当且仅当lambdaexpression的时候,此函数调用运算符被声明为const(9.3.1)   参数声明子句后面没有可变的。

编辑litb的评论: 也许他们想到了按值捕获,以便变量的外部变化不会反映在lambda中?参考文献有两种方式,所以这是我的解释。不知道它是否有用。

编辑kizzx2的评论: 使用lambda时的最多次是作为算法的算符。默认const ness允许它在常量环境中使用,就像普通const一样 - 可以在那里使用限定函数,但非const - 合格函数不能。也许他们只是想让那些知道他们脑子里发生了什么的案件变得更直观。 :)

答案 5 :(得分:12)

您需要考虑Lambda函数的闭包类型是什么。每次声明一个Lambda表达式时,编译器都会创建一个闭包类型,它只是一个带有属性的未命名类声明( environment ,其中声明的Lambda表达式)和函数调用{{1}实施。当您使用按值复制捕获变量时,编译器将在闭包类型中创建一个新的::operator()属性,因此您无法在Lambda表达式中更改它,因为它是一个“只读”属性,这就是他们称之为“闭包”的原因,因为在某种程度上,您通过将变量从上部范围复制到Lambda范围来关闭Lambda表达式。当您使用关键字const时,捕获的实体将成为闭包类型的mutable属性。这是导致由值捕获的可变变量中所做的更改不会传播到较高范围,而是保留在有状态Lambda内的原因。 总是试着想象你的Lambda表达式的结果闭包类型,这对我有很大帮助,我希望它也可以帮到你。

答案 6 :(得分:9)

  

我的印象是   按价值捕获的整点是   允许用户更改临时    - 否则我几乎总是更好地使用按引用捕获,不是   我?

n 是临时的。 n是使用lambda表达式创建的lambda-function-object的成员。默认的期望是调用lambda不会修改其状态,因此它是const以防止您意外修改n

答案 7 :(得分:4)

现在有一项建议可以减少lambda声明中mutable的需要:n3424

答案 8 :(得分:3)

您必须了解捕获的含义!它捕获的不是参数传递!让我们看一些代码示例:

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(lambda内的x仍为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值对lambda进行“突变”,而新值与实际值无关主函数中x的值

答案 9 :(得分:0)

为了扩展Puppy的答案,lambda函数旨在pure functions。这意味着给定唯一输入集的每个调用始终返回相同的输出。在调用lambda时,让我们将输入定义为所有参数的集合以及所有捕获的变量。

在纯函数中,输出完全取决于输入,而不取决于某些内部状态。因此,任何lambda函数(如果是纯函数)都不需要更改其状态,因此是不可变的。

当一个lambda通过引用捕获时,写入捕获的变量是对纯函数概念的一种压力,因为纯函数应该做的就是返回一个输出,尽管lambda不会因为写入发生在外部变量而变异。即使在这种情况下,正确的用法意味着如果再次使用相同的输入调用lambda,则每次输出都是相同的,尽管这些副作用对副变量有影响。这样的副作用只是返回一些额外输入的方法(例如更新计数器),并且可以重新表示为纯函数,例如返回元组而不是单个值。