当前位置: 首页 > 知识库问答 >
问题:

为什么不可变lambda中的字段在捕获常量值或常量引用时使用“const”?

华景明
2023-03-14

如问题所示,值可变的lambda捕获不适用于常量

但是为什么对不可变的lambda也这样做呢?在不可变lambda中,运算符()声明为const,因此它无论如何都不能修改捕获的值。

当我们移动lambda时,这种情况的坏后果就会发生,例如,当我们将lambda包装在一个std::function中时。

请参见以下两个示例:

#include <cstdio>
#include <functional>

std::function<void()> f1, f2;

struct Test {
    Test() {puts("Construct");}
    Test(const Test& o) {puts("Copy");}
    Test(Test&& o) {puts("Move");}
    ~Test() {puts("Destruct");}
};

void set_f1(const Test& v) {
    f1 = [v] () {}; // field type in lambda object will be "const Test"
}

void set_f2(const Test& v) {
    f2 = [v = v] () {}; // field type in lambda object will be "Test"
}

int main() {
    Test t;
    puts("set_f1:");
    set_f1(t);
    puts("set_f2:");
    set_f2(t);
    puts("done");
}

我们得到以下编译器生成的lambda类:

class set_f1_lambda {
    const Test v;
public:
    void operator()() const {}
};

class set_f2_lambda {
    Test v;
public:
    void operator()() const {}
};

程序打印以下内容(使用gcc或clang):

Construct
set_f1:
Copy
Copy
Copy
Destruct
Destruct
set_f2:
Copy
Move
Move
Destruct
Destruct
done
Destruct
Destruct
Destruct

在第一个示例中,set\u f1至少复制了三次v值。

在第二个示例set_f2中,唯一的副本是捕获值时(如预期的那样)。使用两个移动是libstdc中的一个实现细节。当按值传递函子给内部函数时,第一个移动发生在operator=std::function内部(为什么此函数签名不使用按引用传递?)。第二个移动发生在move构造最终的堆分配函子时。

但是,如果字段是常量,lambda functor对象的移动构造函数就不能对该字段使用移动构造函数(因为这样的构造函数在窃取常量变量的内容后无法“清除”该变量)。这就是为什么这些字段必须使用复制构造函数的原因。

所以对我来说,在不可变的lambdas中捕获值作为const似乎只有负面影响。我是错过了一些重要的东西,还是只是以这种方式标准化,使标准更加简单?

共有1个答案

赵嘉悦
2023-03-14

我是否遗漏了一些重要的东西,或者只是通过这种方式进行了标准化,以使标准更加简单?

最初的lambda提案,

  • N2550: Lambda表达式和闭包:单形Lambdas的用词(修订版4)

区分捕获对象的类型和lambda闭包类型的相应数据成员的类型:

/6闭包对象的类型是一个具有唯一名称的类,称为F,被认为是在lambda表达式出现的地方定义的。

在lambda表达式出现的上下文中查找有效捕获集中的每个名称N,以确定其对象类型;对于引用,对象类型是引用引用的类型。对于有效捕获集中的每个元素,F都有一个私有的非静态数据成员,如下所示:

  • 如果元素是this,那么数据成员有一些唯一的名称,称之为t,并且是this类型([class.this],9.3.2)
  • 如果元素的形式为

在最初的措辞中,OP的示例不会导致const限定的数据成员v。我们还可以指出,我们认识到措辞

对于引用,对象类型是引用引用的类型

在lambdas的最终措辞(最新草案)的[expr.prim.lambda.capture]/10中(但直接说明数据成员的类型,而不是对象类型):

如果实体是对对象的引用,则此类数据成员的类型为引用类型;如果实体是对函数的引用,则此类数据成员的类型为对引用函数类型的左值引用;否则,则此类数据成员的类型为相应捕获实体的类型。

发生的事是

  • N2927:C 0x Lambdas的新措辞(第2版)

重新编写了N2550中的大部分措辞:

在2009年3月首脑会议期间,核心工作组(CWG)提出并审查了大量与C/D相关的问题。在为这些问题中的大多数确定了明确的方向后,CWG得出结论,最好重写关于Lambdas的部分,以实现该方向。本文介绍了这种重写。

特别是在这个问题的背景下,解决CWG问题

  • CWG 756。在闭包对象的成员上删除cv限定

[...] 考虑下面的例子:

void f() {
  int const N = 10;
  [=]() mutable { N = 30; }  // Okay: this->N has type int, not int const.
  N = 20;  // Error.
}

也就是说,作为闭包对象成员的N不是const,即使捕获的变量是const。这似乎很奇怪,因为捕获基本上是一种捕获本地环境的方法,可以避免生命周期问题。更严重的是,类型的更改意味着应用于lambda表达式中捕获的变量的decltype、重载解析和模板参数推断的结果可能与包含lambda表达式的范围中的结果不同,这可能是一个微妙的bug源。

在这之后(从N2927开始)的措辞变成了我们最后看到的C 11

这种数据部件的类型是如果实体不是对对象的引用则捕获的相应实体的类型,或者以其他方式引用的类型。

如果我敢猜测,CWG 756的决议还意味着保留参考类型实体价值捕获的cv限定符,这可能是一种疏忽。

 类似资料:
  • 考虑以下几点: 为什么第一个版本是编译错误,当我已经声明lambda是可变的,并通过值捕获(我认为是它的副本)? 使用clang(x86_64-apple-darwin14.3.0)和Visual C(vc120)进行测试,这是错误消息的来源。

  • 问题内容: 如果您查看字节码 Java 8更新121生成的字节码是 该方法被调用的,结果被忽略。 这是间接的空引用检查吗? 当然,如果您跑步 这将触发NullPointerException。 问题答案: 是的,调用已成为规范的“测试”成语,正如预期的那样,这是一种廉价的内在操作,而且我想,如果未使用。 另一个示例是使用不是的外部实例创建内部类实例: 编译为 另请参阅JDK-8073550: 我们

  • 问题内容: 在什么情况下,您将使用字段变量而不是局部变量?我发现很难决定何时在类中的2个或更多方法中使用变量。我倾向于使用局部变量,并将它们传递给另一种方法。 谢谢, 莎拉 问题答案: 用面向对象的术语来说,变量作为对象的属性有意义吗?如果是这样,则应将其设为字段变量。如果没有,它可以任意选择。 记住单一责任原则-设计良好的班级应该只承担1个责任,因此只有1个改变的理由。

  • 问题内容: 有一些关于如何为Swift 编写代码以及如何在Swift中编写代码的文章。但尚不清楚何时使用和而不是和。有人可以解释吗? 问题答案: 当您在类(或结构)中定义静态var / let时,该信息将在所有实例(或值)之间共享。 分享信息 如您在这里看到的,我创建了2个单独的实例,但是它们确实共享相同的静态变量。 辛格尔顿 通常使用静态常数来采用Singleton模式。在这种情况下,我们希望分

  • 变量是计算机内存中已命名的存储位置,其中包含了数字或字符串等数据。变量包含的信息被称为变量的值。变量使用用户便于理解脚本操作的名称为用户提供了一种存储、检索和操作数据的途径。 声明和命名变量 命名和声明变量应遵循脚本语言的规则及指导。即使在使用变量前不需要去声明变量,也应养成在编程时声明变量的良好习惯,因为这样有助于防止错误发生。声明一个变量意味着告诉脚本引擎,有一个特定名称的变量,这样就可以在脚

  • 问题内容: 如何引用常量而不是函数级别变量? 问题答案: 你不能 当局部变量在作用域内时,名称表示局部变量。并且没有“限定符”来引用顶级标识符。 规格:声明和范围: 可以在内部块中重新声明在块中声明的标识符。内部声明的标识符在范围内,它表示内部声明声明的实体。 如果您需要同时访问顶级常量/变量和局部变量,请使用不同的名称。 如果由于某种原因您不愿意或不想这样做,可以先保存顶级常量或变量的值: 或者