可以在可变 lambda 中更改 this 吗?

Can `this` be changed in a mutable lambda?

大家都知道this C++中的对象指针不能在它的方法中改变。但是对于捕获 this 的可变 lambda,一些当前的编译器提供了这种可能性。考虑这段代码:

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,这意味着它是 [=17= 的副本] 指针。在 thisconst 限定的上下文中,副本自然不能用于更改 this(即使 lambda 是可变的;与 'point to const' 比较),但是 as lambda 是可变的,指针(副本)可以用来指向不同的东西,例如nullptr.

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:

的大小写
  • 一个capture-list包含一个capture
  • A capturesimple-captureinit-capture;我们忽略后一种情况,因为它在上面已经介绍过
  • A simply-capture 具有以下形式之一:
    • 标识符 ...选择
    • &标识符 ...opt
    • this
    • *this

根据 [expr.prim.lambda.capture]/10 [强调 我的]:

An entity is captured by copy if

  • (10.1) it is implicitly captured, the capture-default is =, and the captured entity is not *this, or

  • (10.2) it is explicitly captured with a capture that is not of the form this, & identifier, or & identifier initializer.

只有 simple-capture 形式 *this 允许通过复制显式捕获 *this 对象。然而,简单捕获 this 通过引用捕获 *this 对象 (+),根据 [expr.prim.lambda.capture]/12:

(+) simple-capture:s this*this 都表示本地实体 *this , 根据 [expr.prim.lambda.capture]/4.

An entity is captured by reference if it is implicitly or explicitly captured but not captured by copy. It is unspecified whether additional unnamed non-static data members are declared in the closure type for entities captured by reference. [...]

因此:

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]/1this 不是可修改的左值,这就是 #2 格式错误的原因:

In the body of a non-static ([class.mfct]) member function, the keyword this is a prvalue whose value is a pointer to the object for which the function is called. The type of this in a member function whose type has a cv-qualifier-seq cv and whose class is X is “pointer to cv X”. [...]

根据 [expr.prim.lambda.closure]/12,这也适用于在 lambda 中使用 this 的情况:

The lambda-expression's compound-statement yields the function-body ([dcl.fct.def]) of the function call operator, but for purposes of name lookup, determining the type and value of this and transforming id-expressions referring to non-static class members into class member access expressions using (*this) ([class.mfct.non-static]), the compound-statement is considered in the context of the lambda-expression.

MSVC 接受您的代码段是错误的(接受无效)。

事实上,在下面的例子中 (demo):

#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,如:

[...] The type of this in a member function whose type has a cv-qualifier-seq cv and whose class is X is “pointer to cv X” [...]

... 而不是“指向 cv X 的常量指针”(首先,纯右值的常量性很奇怪)。