为什么c++ 11's lambda要求&;mutable&;关键字按值捕获,默认情况下?

短的例子:

#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。这应该是标准的)

90419 次浏览

参见5.1.2下的该草案 [expr.prim. exe]。,第5款:

lambda表达式的闭包类型有一个公共内联函数调用操作符(13.5.4),其参数 和返回类型由lambda表达式的参数声明子句和trailingreturn-描述 类型分别。此函数调用操作符声明为const(9.3.1)当且仅当lambdaexpression为 参数声明-子句后面不跟mutable

编辑litb的评论: 也许他们想到了按值捕获,这样外部对变量的更改就不会反映在lambda中?引荐是双向的,这就是我的解释。但我不知道这是否有用。< / p >

编辑kizzx2的评论: 使用lambda的大多数情况下是作为算法的函子。默认的constness允许它在常量环境中使用,就像普通的const-qualified函数可以在那里使用一样,但非-const-qualified函数不能使用。也许他们只是想让这些情况更直观,他们知道自己在想什么。:) < / p >

在我的印象中,按值捕获的全部意义在于允许用户更改临时对象——否则我几乎总是使用按引用捕获更好,不是吗?

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

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


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

我认为mutable不是一个“几乎”的情况。我认为“按值捕获”就像“允许我在捕获的实体死亡后使用它的值”,而不是“允许我更改它的副本”。但这或许是有争议的。

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

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

我的印象是 价值捕获的关键在于 允许用户更改临时 —否则,我几乎总是更好地使用引用捕获,而不是 我吗?< / p >

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

你的代码几乎相当于:

#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时被初始化,就像有一个隐藏的构造函数一样。

现在有一个建议来减轻在lambda声明中mutable的需求:n3424

FWIW, c++标准化委员会的知名成员Herb Sutter在正确性和可用性问题中对这个问题给出了不同的答案:

考虑这个稻草人的例子,其中程序员通过捕获一个局部变量 值,并尝试修改 捕获值(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中应该改变这一点。它很短,写得很好,如果你想知道关于这个特定的特性“委员们在想什么”,值得一读。

为了扩展Puppy的回答,lambda函数被设计为纯函数。这意味着给定唯一输入集的每次调用总是返回相同的输出。让我们将输入定义为调用lambda时所有参数加上所有捕获变量的集合。

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

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

你必须理解捕获的意思!这是捕获而不是参数传递!让我们看一些代码示例:

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指定为mutable,就意味着对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的实际值无关

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

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

输出:

call1:6
call2:11

单词:

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

    lambda对象按值保存捕获的变量(有自己的变量) . copy)在[=]情况下
  1. lambda可以被调用多次。

在一般情况下,我们必须有相同的捕获变量的值,基于已知的捕获值,有相同的可预测的lambda行为,而不是在lambda工作期间更新。这就是为什么默认行为假设const(预测lambda对象成员的变化),当用户意识到后果时,他会用mutable对自己负责。

与按值捕获相同。举个例子:

auto lbd = [x]() mutable { return x += 5; };

如果你检查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

< >强案例2: 在这里,当您通过值捕获参数并使用'mutable'关键字时,与第一种情况类似,您创建了一个"copy"关于这个论点。这个“copy"生活在“世界”;的值,但是现在,你可以在-世界中修改参数,所以它的值会被改变,并保存,在将来调用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不再有两条生命。现在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