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

问题描述 投票:231回答:10

简短的例子:

#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)

c++ lambda c++11
10个回答
217
投票

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


0
投票

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

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

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


99
投票

你的代码几乎等同于:

#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,除非你说它是可变的。

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


35
投票

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

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

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

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

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


27
投票

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中改变它。它简短,写得很好,如果你想知道“关于这个特殊功能的[委员会成员]的想法”,那就值得一读。


15
投票

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

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

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

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


12
投票

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


10
投票

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

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


4
投票

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


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(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在每次调用时“变异”lambda,使用新的x值,与主函数中的x的实际值无关

© www.soinside.com 2019 - 2024. All rights reserved.