为什么非可变 lambda 中的字段在捕获 const 值或 const 引用时使用 "const"?

Why do fields in non-mutable lambdas use "const" when capturing const values or const references?

如问题 中所示,当在可变 lambda 中使用其名称或 [=] 捕获类型 const T& 的值时,隐藏字段 class 获取类型 const T。可以说这是对可变 lambda 的正确做法。

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

当我们移动 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 classes:

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

第一个例子v的值被复制不少于三次set_f1

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

但是,lambda 仿函数对象的移动构造函数不能对字段使用移动构造函数,如果字段是 const(因为这样的构造函数不能在之后“清除”const 变量窃取其内容)。这就是为什么必须对此类字段使用复制构造函数。

所以对我来说,在非可变 lambda 中将值捕获为 const 似乎只会产生负面影响。我是否遗漏了一些重要的东西,或者它只是通过这种方式进行了标准化,以某种方式使标准更简单?

Did I miss something important, or has it simply been standardised this way to make the standard more simpler somehow?

最初的 lambda 提案,

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

/6 The type of the closure object is a class with a unique name, call it F, considered to be defined at the point where the lambda expression occurs.

Each name N in the effective capture set is looked up in the context where the lambda expression appears to determine its object type; in the case of a reference, the object type is the type to which the reference refers. For each element in the effective capture set, F has a private non-static data member as follows:

  • if the element is this, the data member has some unique name, call it t, and is of the type of this ([class.this], 9.3.2);
  • if the element is of the form & N, the data member has the name N and type “reference to object type of N”; 5.19. CONSTANT EXPRESSIONS 3
  • otherwise, the element is of the form N, the data member has the name N and type “cv-unqualified object type of N”.

在这个原始措辞中,OP 的示例不会产生 const 限定的数据成员 v。我们可能还会注意到,我们识别

的措辞

in the case of a reference, the object type is the type to which the reference refers

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

The type of such a data member is the referenced type if the entity is a reference to an object, an lvalue reference to the referenced function type if the entity is a reference to a function, or the type of the corresponding captured entity otherwise.

发生的事情是

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

During the meeting of March 2009 in Summit, a large number of issues relating to C++0x Lambdas were raised and reviewed by the core working group (CWG). After deciding on a clear direction for most of these issues, CWG concluded that it was preferable to rewrite the section on Lambdas to implement that direction. This paper presents this rewrite.

特别是对于这个问题的上下文,解决 CWG 问题

[...] Consider the following example:

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

That is, the N that is a member of the closure object is not const, even though the captured variable is const. This seems strange, as capturing is basically a means of capturing the local environment in a way that avoids lifetime issues. More seriously, the change of type means that the results of decltype, overload resolution, and template argument deduction applied to a captured variable inside a lambda expression can be different from those in the scope containing the lambda expression, which could be a subtle source of bugs.

之后,措辞(从 N2927 开始)变成了我们看到的最终进入 C++11 的措辞

The type of such a data member is the type of the corresponding captured entity if the entity is not a reference to an object, or the referenced type otherwise.

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