在默认情况下,为什么C ++ 11的lambda要求“ mutable”关键字进行按值捕获?


256

简短示例:

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


101
好问题; 尽管我很高兴终于const可以默认设置某些东西了!
xtofl 2011年

3
这不是一个答案,但是我认为这是明智的选择:如果您按值取值,则不应该仅仅为了将1个副本保存到局部变量而对其进行更改。至少您不会通过将=替换为&来更改n的错误。
stefaanv 2011年

8
@xtofl:如果const默认情况下不是所有其他功能,则不确定是否很好。
kizzx2 2011年

8
@TamásSzelei:不用争论,但是恕我直言,“易学”的概念在C ++语言中没有地位,尤其是在现代。无论如何:P
kizzx2 2011年

3
“按值捕获的全部要点是允许用户更改临时值”-不,要点是,lambda可能在任何捕获的变量的生存期之后仍然有效。如果C ++ lambda仅具有按引用捕获功能,则它们在太多情况下将无法使用。
塞巴斯蒂安·

Answers:


230

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


7
这是个好的观点。我完全同意。但是在C ++ 0x中,我不太清楚默认值如何帮助实现上述要求。考虑我在lambda的接收端,例如我void f(const std::function<int(int)> g)。我如何保证它g实际上是参照透明的g的供应商可能已经使用过mutable。所以我不知道。另一方面,如果默认值为non- const,并且人们必须添加const而不是mutable向函数对象添加代码,则编译器可以实际执行该const std::function<int(int)>部分,现在f可以假定gconst,不是吗?
kizzx2 2011年

8
@ kizzx2:在C ++中,不强制执行任何操作,仅建议执行。和往常一样,如果您做一些愚蠢的事情(记录透明性要求,然后再传递非参照透明功能),您将得到什么。
小狗

6
这个答案使我大开眼界。以前,我认为在这种情况下,lambda仅会更改当前“运行”的副本。
Zsolt Szatmari 2015年

4
@ZsoltSzatmari您的评论让我大开眼界!:-DI在我读完您的评论之前,并没有得到这个答案的真正含义。
詹达斯(Jendas)2015年

5
我不同意这个答案的基本前提。C ++在语言的其他任何地方都没有“函数应始终返回相同的值”的概念。作为一种设计原则,我同意这是编写函数的好方法,但是我认为它不能成为标准行为理由。
Ionoclast Brigham

103

您的代码几乎与此等效:

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


5
虽然一个很好的解释了什么一个constmutable拉姆达会是什么样子,如果实现等效用户定义类型,问题是(如标题和评论由OP阐述)为什么 const是默认的,所以这并不回答。
underscore_d

36

我给人的印象是,按值捕获的全部目的是允许用户更改临时值-否则,使用按引用捕获几乎总是更好,不是吗?

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

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

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

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


2
好的例子。对于按值捕获,这是一个非常有力的用例。但是为什么默认为const?它能达到什么目的?mutable显得格格不入这里的时候const不是在“几乎”默认(:P),一切的语言。
kizzx2 2011年

8
@ kizzx2:我希望const是默认设置,至少人们会被迫考虑const- 正确性:/
Matthieu M.

1
@ kizzx2调查了lambda文件,在我看来,他们将其默认设置为,const因此无论lambda对象是否为const,他们都可以调用它。例如,他们可以将其传递给带有的函数std::function<void()> const&。为了允许lambda更改其捕获的副本,在最初的论文中,关闭的数据成员是在mutable内部自动定义的。现在,您必须手动mutable输入lambda表达式。我还没有找到详细的理由。
Johannes Schaub-litb 2011年


5
在这一点上,对我来说,“真实”的答案/理由似乎是“他们未能解决实现细节”:/
kizzx2 2011年

32

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

添加此功能的原因是担心用户可能不会意识到自己获得了副本,尤其是由于Lambda是可复制的,因此他可能会更改其他Lambda的副本。

他的论文是关于为什么要在C ++ 14中对此进行更改。如果您想了解“特定委员会对此事的看法”,那么简短易懂,值得一读。


16

您需要考虑一下Lambda函数的闭包类型是什么。每次声明Lambda表达式时,编译器都会创建一个闭包类型,该闭包类型就是具有属性(声明Lambda表达式的环境)和函数调用::operator()实现的未命名类声明。当您使用copy-by-value捕获变量时,编译器将const在闭包类型中创建一个新属性,因此您无法在Lambda表达式中更改它,因为它是“只读”属性,这就是它们的原因之所以称其为“ 闭包 ”,是因为通过某种方式,您是通过将变量从较高范围复制到Lambda范围来关闭Lambda表达式的。mutable,捕获的实体将成为non-const您的关闭类型的属性。这就是导致按值捕获的可变变量中所做的更改不会传播到上限,而是保留在有状态Lambda中的原因。始终尝试想象一下Lambda表达式的最终闭合类型,这对我有很大帮助,我希望它也能对您有所帮助。


14

参见本草案,在5.1.2 [expr.prim.lambda]下,第5节:

lambda表达式的闭包类型具有公共内联函数调用运算符(13.5.4),其参数和返回类型分别由lambda表达式的parameter-declaration-clause和Trailingreturn-type描述。当且仅当lambdaexpression的parameter-declaration-clause不跟随可变变量时,此函数调用运算符才声明为const(9.3.1)。

编辑litb的评论:也许他们想到了按值捕获,以便变量的外部更改不会反映在lambda内部?参考是双向的,这就是我的解释。不知道这有什么好处。

编辑kizzx2的评论:使用lambda的大多数时间是作为算法的函子。默认的默认const性允许它在恒定的环境中使用,就像const可以在其中使用普通的合格函数一样,而在非const合格函数中则不能使用。也许他们只是想让这些案例更直观,因为他们知道自己的想法。:)


这是标准,但是为什么他们要这样写呢?
kizzx2 2011年

@ kizzx2:我的解释就在那句话的下面。:)它与litb关于捕获的对象的生存期的说法有些关系,但还有一点。
Xeo

@Xeo:哦,是的,我很想念:P这也是很好地使用按值捕获的另一个很好的解释。但是为什么const默认情况下呢?我已经有了新的副本,不让我更改它似乎很奇怪-尤其是它在本质上没有什么错-他们只是想让我添加mutable
kizzx2 2011年

我相信有人试图创建一种新的通用函数声明语法,看起来很像一个命名的lambda。还应该通过默认设置所有常量来解决其他问题。从未完成,但想法在lambda定义上逐渐消失。
Bo Persson

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

11

我给人的印象是,按值捕获的全部目的是允许用户更改临时值-否则,使用按引用捕获几乎总是更好,不是吗?

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


1
整个lambda对象是一个临时对象,其成员也具有临时寿命。
Ben Voigt 2014年

2
@Ben:IIRC,我指的是当有人说“临时”时,我理解它的意思是未命名的临时对象,lambda本身是,但成员不是。从lambda的“内部”开始,lambda本身是否是临时的也没有关系。重新阅读该问题,尽管看起来OP只是在说“临时”时说“ lambda中的n”。
Martin Ba

6

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

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已更改为20lambda仍返回10(x仍在5lambda x内)在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

}

上面的示例显示,通过使lambda可变,x在lambda内部进行更改会在每次调用时对lambda进行“突变”,x使其具有新的值与xmain函数中的实际值无关


4

现在有一个建议可以减轻对mutablelambda声明的需要:n3424


有什么信息吗?我个人认为这是一个坏主意,因为新的“捕获任意表达式”可以消除大多数问题。
Ben Voigt

1
@BenVoigt是的,似乎是为了改变而改变。
Miles Rout 2014年

3
@BenVoigt尽管公平地说,我希望可能有很多C ++开发人员mutable甚至都不知道C ++中的关键字。
Miles Rout 2014年

1

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

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.