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

kiz*_*zx2 249 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"
}
Run Code Online (Sandbox Code Playgroud)

问题:为什么我们需要mutable关键字?它与传统参数传递给命名函数有很大不同.背后的理由是什么?

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

有什么启示吗?

(顺便说一句,我使用的是MSVC2010.这应该是标准的AFAIK)

Pup*_*ppy 226

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

  • @ kizzx2:在C++中,没有*强制执行*,只是建议.按照惯例,如果你做了一些愚蠢的事情(记录了引用透明度的要求,然后传递非引用透明的函数),你会得到任何东西. (8认同)
  • 这是个好的观点.我完全同意.但是在C++ 0x中,我不太清楚默认如何帮助强制执行上述操作.考虑一下我在lambda的接收端,例如我是`void f(const std :: function <int(int)> g)`.我如何保证`g`实际上是_referentially transparent_?无论如何,'g`的供应商可能已经使用了'mutable`.所以我不知道.另一方面,如果默认为非`constst',并且人们必须将`const`而不是`mutable`添加到函数对象,编译器实际上可以强制执行`const std :: function <int(int)>`部分现在`f`可以假设`g`是`const`,不是吗? (7认同)
  • 这个答案让我大开眼界.以前,我认为在这种情况下,lambda只会改变当前"run"的副本. (5认同)
  • “函数对象每次被调用时都应该产生相同的结果。” 不,虽然这个说法对于函数式编程语言来说可能是正确的,并且也恰好是良好的编程实践,但 C++ 是一种多范式语言,而不是“唯一”的函数式语言。因此,有人可能会争辩说,标准委员会无权将函数式编程风格“强加”给 C++ 程序员。 (4认同)
  • @ZsoltSzatmari你的评论让我眼前一亮!:在我读你的评论之前,-DI没有得到这个答案的真正含义. (3认同)
  • 我不同意这个答案的基本前提.C++在语言的任何其他地方都没有"函数应该总是返回相同的值"的概念.作为一个设计原则,我同意这是一个编写函数的好方法,但我不认为它是水作为标准行为的理由. (3认同)
  • 完全同意@lonoclastBrigham,但不同意这个答案。请不要教条地声明最佳实践是标准中技术决策的原因,除非您实际上参与了标准委员会的讨论(如果是这样,请在您的答案中说明)。 (3认同)
  • 我必须阅读其他答案才能理解这个人的想法.这个答案太简洁了,只对那些已经了解正在发生的事情的人有用. (3认同)
  • 说真的我听不懂你在说什么。例如,`int i = 1; 自动 f = [i](){++i;}; F(); F(); F(); f();` 如果这不是非法的,调用四次 f() 会产生相同的结果,对吧?除非,lambda 中的 i 和外部 i 是同一个对象。所以,问题变成了:当按值捕获时,lambda 中捕获的对象和外部对象是否是同一个对象?好吧,我认为这不是因为它是按值捕获,而不是按引用捕获。那么为什么我们在使用按值捕获时不能改变捕获的值呢?我认为丹尼尔的回答更好。 (2认同)
  • 问题中给出的示例将通过“[=]() mutable {++n;}();”更好地阐明实际点,因为“n=20”实际上每次都会产生相同的结果,因此有必要“可变”的不太明显。另请注意 @ZsoltSzatmari 的评论以进一步澄清。 (2认同)

Dan*_*noz 101

你的代码几乎等同于:

#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"
}
Run Code Online (Sandbox Code Playgroud)

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

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

  • 虽然对_what_一个`const`或`mutable` lambda的一个很好的解释看起来好像是作为等效的用户定义类型实现的,但问题是(如标题中的OP并在评论中详细阐述)**为什么**`const`是默认值,所以这不会回答它. (5认同)

Joh*_*itb 36

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

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

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

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

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

  • @ kizzx2:我希望`const`是默认值,至少人们会被迫考虑const-correctness:/ (8认同)
  • 在这一点上,对我来说,"真正的"答案/理由似乎是"他们无法解决实施细节":/ (5认同)
  • @kizzx2 查看 lambda 论文,在我看来,他们将其默认为 `const`,因此无论 lambda 对象是否为 const,他们都可以调用它。例如,他们可以将其传递给采用 `std::function&lt;void()&gt; const&amp;` 的函数。为了允许 lambda 更改其捕获的副本,在最初的论文中,闭包的数据成员在内部自动定义为“可变的”。现在您必须手动将 `mutable` 放入 lambda 表达式中。不过我还没有找到详细的理由。 (3认同)
  • 好例子.这是使用按值捕获的非常强大的用例.但为什么它默认为`const`?它的目的是什么?`mutable`在这里似乎不合适,当`const`是*not*默认在"几乎"(:P)语言的其他部分. (2认同)
  • 有关详细信息,请参见http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2008/n2651.pdf. (2认同)

aki*_*kim 28

FWIW,Herb Sutter,C++标准化委员会的知名成员,在Lambda正确性和可用性问题中为该问题提供了不同的答案:

考虑这个稻草人示例,程序员通过值捕获局部变量并尝试修改捕获的值(这是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’
Run Code Online (Sandbox Code Playgroud)

此功能似乎是由于用户可能没有意识到他有副本而引起的,特别是因为lambdas是可复制的,所以他可能正在更改不同的lambda副本.

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


Sou*_*mar 17

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

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

}
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,即使x已更改为20lambda 仍然返回 10(x仍在5lambdax内)在 lambda 内更改意味着在每次调用时更改 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

}
Run Code Online (Sandbox Code Playgroud)

上面的例子表明,通过使 lambda 可变,x在每次调用时改变lambda 内部的 lambda 用一个新值xx主函数中的实际值无关

  • 我比其他人更喜欢你的答案。进一步添加 lambda = 函数 + 环境/范围。定义 lambda 时选择环境。C++ 提供了环境的概念,包括不可变副本、可变副本或共享环境。 (2认同)
  • 这是这里最好的答案。为我清理了很多事情。 (2认同)

Xeo*_*Xeo 14

参见5.1.2 [expr.prim.lambda]下的这一草案,第5节:

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

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

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

  • @ kizzx2-如果我们可以重新开始,我们可能会使用var作为关键字来允许更改,而常数是其他所有内容的默认值。现在我们不这样做了,所以我们必须忍受这一点。考虑到所有因素,IMO,C ++ 2011的发布相当不错。 (2认同)

Tar*_*ula 13

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


Mar*_* Ba 10

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

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

  • @Ben:IIRC,我指的是当有人说"临时"的时候,我理解它是指*未命名的*临时对象,lambda本身就是这样,但它的成员不是.而且从lambda的"内部"开始,lambda本身是否是临时的并不重要.重新阅读这个问题,虽然看起来OP只是想说"临时"中的"n内部". (2认同)

Att*_*son 7

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

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

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


小智 6

我也想知道为什么[=]需要显式的最简单的解释mutable是在这个例子中:

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

输出:

call1:6
call2:11
Run Code Online (Sandbox Code Playgroud)

用言语来说:

您可以看到x第二次调用时的值不同(call1 为 1,call2 为 6)。

  1. lambda 对象按值保留捕获的变量(有自己的副本),以防[=].
  2. lambda 可以被调用多次。

在一般情况下,我们必须具有相同的捕获变量值,才能根据已知的捕获值具有相同的 lambda 可预测行为,而不是在 lambda 工作期间更新。这就是为什么采用默认行为const(预测 lambda 对象成员的更改),并且当用户意识到后果时,他会自行承担此责任mutable

与按值捕获相同。以我为例:

auto lbd = [x]() mutable { return x += 5; };
Run Code Online (Sandbox Code Playgroud)


Yan*_*v G 6

如果您检查 lambda 的 3 个不同用例,您可能会发现其中的差异:

  • 按值捕获参数
  • 使用“mutable”关键字按值捕获参数
  • 通过引用捕获参数

情况 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
Run Code Online (Sandbox Code Playgroud)

情况 2: 在这里,当您按值捕获参数并使用“mutable”关键字时,与第一种情况类似,您将创建该参数的“副本”。这个“副本”存在于 lambda 的“世界”中,但现在,您实际上可以在 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
Run Code Online (Sandbox Code Playgroud)

情况 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
Run Code Online (Sandbox Code Playgroud)