可以在可变lambda中更改`this`吗?
大家都知道thisC++中的对象指针在其方法中是不能改变的。但是对于可变的 lambda,在何处this捕获,一些当前的编译器提供了这种可能性。考虑这个代码:
struct A {
void foo() {
//this = nullptr; //error everywhere
(void) [p = this]() mutable {
p = nullptr; //#1: ok everywhere
(void)p;
};
(void) [this]() mutable {
this = nullptr; //#2: ok in MSVC only
};
}
};
在第一个 lambda 中this被捕获并赋予一个新名称p。这里所有的编译器都允许更改p. 在第二个 lambda 中this被自己的名字捕获,只有 MSVC 允许程序员改变它的值。演示:https : //gcc.godbolt.org/z/x5P81TT4r
我相信 MSVC 在第二种情况下行为不正确(尽管它看起来像一个不错的语言扩展)。谁能从标准中找到正确的措辞(搜索并不容易,因为该词this在那里被提及了 2800 多次)?
回答
初始捕获 this
(void) [p = this]() mutable { p = nullptr; //#1: ok everywhere (void)p; };
这使用 init-capture按值捕获this指针,根据[expr.prim.lambda.capture]/6,这意味着它是指针的副本。在上下文中被-qualified,复制可以自然不能用来改变(即使拉姆达是可变的;具有“指向常量”比较),但作为拉姆达是可变的指针(复制)可用于指向不同的东西,例如。thisthisconstthisnullptr
struct S {
void f() const {
(void) [p = this]() mutable {
p->value++; // ill-formed: 'this' is pointer to const, meaning
// 'p' is pointer to const.
p = nullptr; // OK: 'p' is not const pointer
(void)p;
};
}
void f() {
(void) [p = this]() mutable {
p->value++; // OK: 'this' is pointer to non-const, meaning
// 'p' is pointer to non-const.
p = nullptr; // OK: 'p' is not const pointer
(void)p;
};
}
int value{};
};
简单的捕获 this
(void) [this]() mutable { this = nullptr; //#2: ok in MSVC only };
根据[expr.prim.lambda.capture],忽略capture-default :s 的情况:
- 一个捕获列表包含一个捕获
- 甲捕获可以是一个简单的捕获或INIT-捕获; 我们忽略上面提到的后一种情况
- 一个简单的捕获具有下列形式之一:
- 标识符...选择
&标识符...选择this*this
根据[expr.prim.lambda.capture]/10 [强调我的]:
一个实体被副本捕获,如果
(10.1) 它是隐式捕获的,捕获默认为
=,捕获的实体不是*this,或(10.2) 它是用不是
this、&identifier或&identifier initializer形式的捕获显式捕获的。
只有简单捕获形式*this允许*this通过复制显式捕获对象。this然而,简单的 capture是通过引用捕获*this对象(+),根据[expr.prim.lambda.capture]/12:
(+) simple-capture :sthis和*this都表示本地实体*this,如[expr.prim.lambda.capture]/4。
如果实体被隐式或
显式捕获但未被 copy捕获,则通过引用捕获该实体。对于通过引用捕获的实体,是否在闭包类型中声明了其他未命名的非静态数据成员是未指定的。[...]
因此:
struct S {
void f() const {
(void) [this]() mutable {
// '*this' explicitly-captured by-reference
this->value++; // ill-formed: 'this' is pointer to const
this = nullptr; // #2 ill-formed: 'this' is not a modifyable lvalue
};
}
void f() {
(void) [this]() mutable {
// '*this' explicitly-captured by-reference
this->value++; // OK: 'this' is pointer to non-const
this = nullptr; // #2 ill-formed: 'this' is not a modifyable lvalue
};
}
int value{};
};
根据[class.this]/1,this不是可修改的左值,这就是为什么#2:
在非静态 ([class.mfct]) 成员函数的主体中,关键字
this是一个纯右值,其值是指向调用该函数的对象的指针。的类型的this在一个成员函数,其类型具有CV-限定符-SEQ CV和它的类是X为“指向cv X”。[...]
其中,根据[expr.prim.lambda.closure]/12也适用于 whenthis在 lambdas 中使用:
所述λ-表达的化合物语句产生的函数调用操作的功能体([dcl.fct.def]),但为的目的名称查找,确定所述类型和值的
this和转化ID表达式参照非静态使用 (*this) ([class.mfct.non-static]) 将类成员转换为类成员访问表达式,将则在 lambda 表达式的上下文中考虑复合语句。
MSVC 接受您的代码段是不正确的(接受无效)。
事实上,在以下示例(演示)中:
#include <iostream>
struct S;
void g(S *& ) { std::cout << "lvalue pointer to S"; }
void g(S *&& ) { std::cout << "rvalue pointer to S"; }
struct S {
void f() {
auto l = [this]() { g(this); };
l();
}
};
int main() {
S s{};
s.f();
}
我们期望第二个g重载是一个更好的匹配,就像this一个纯右值。然而,虽然 GCC 和 Clang 的行为符合预期
// GCC & Clang: rvalue pointer to S
MSVC 甚至无法编译程序
// MSVC: error, no viable overload; arg list is '(S *const )'
这违反了 [class.this]/1,因为
[...]
this类型具有 cv-qualifier-seq cv 且类X为“指向 cv X 的指针”的成员函数中的类型[...]
... 而不是“指向 cv X 的常量指针”(首先,纯右值上的常量会很奇怪)。