【问题标题】:Can `this` be changed in a mutable lambda?可以在可变 lambda 中更改“this”吗?
【发布时间】:2021-10-19 02:03:55
【问题描述】:

大家都知道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 被复制捕获,因此更改它不会更改原始指针。这应该始终被允许。在第二种情况下,this 是通过引用捕获的,因为[this] 是一种特殊情况。

标签: c++ lambda language-lawyer this


【解决方案1】:

this 的初始化捕获

(void) [p = this]() mutable { 
    p = nullptr; //#1: ok everywhere
    (void)p;
};

这使用初始化捕获来捕获this 指针按值,根据[expr.prim.lambda.capture]/6,这意味着它是this 指针的副本。在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
  • capturesimple-captureinit-capture;我们忽略后一种情况,因为它已经在上面介绍过
  • simply-capture 具有以下形式之一:
    • 标识符 ...选择
    • &标识符 ...opt
    • this
    • *this

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

一个实体被副本捕获如果

  • (10.1) 隐式捕获,捕获默认为=,捕获的实体不是*this,或者

  • (10.2) 使用不是this&标识符&标识符形式的捕获显式捕获它初始化器.

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

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

一个实体被引用捕获,如果它是隐式或 明确捕获但未通过副本捕获。未指定是否在 通过引用捕获的实体的闭包类型。 [...]

因此:

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 格式错误:

在非静态 ([class.mfct]) 成员函数的主体中,关键字this 是纯右值,其值是指向调用该函数的对象的指针.类型为 cv-qualifier-seq cv 且类为 X 的成员函数中 this 的类型是“指向 cv X 的指针”。 [...]

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

lambda 表达式的复合语句产生函数调用运算符的函数体 ([dcl.fct.def]),但 出于名称查找的目的,确定类型this 的值 并使用 (*this) ([class.mfct.non-static]) 将引用非静态类成员的 id 表达式转换为类成员访问表达式, 复合语句在 lambda 表达式的上下文中被考虑。

MSVC 接受你的 sn-p 是不正确的(accepts-invalid)。

事实上,在以下示例中 (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 是prvalue。然而,虽然 GCC 和 Clang 的行为符合预期:

// GCC & Clang: rvalue pointer to S

MSVC 甚至无法编译程序:

// MSVC: error, no viable overload; arg list is '(S *const )'

这违反了[class.this]/1,为:

[...] 类型为 cv-qualifier-seq cv 且类为X 的成员函数中this 的类型是“指向cv X 的指针” [ ...]

... 而不是“指向 cv X 的常量指针”(prvalue 上的常量会很奇怪,首先)。

【讨论】:

  • 您写了“...这就是为什么 #1 格式错误”,但您的意思是“#2 格式错误”,不是吗?
  • @dfrib 啊,明白了。 :-)
  • 我认为[this] 完全符合您的预期:它捕获this "by-copy"... 除了this 只是指向当前方法的接收器的指针,所以这就像通过引用捕获该对象。因此使用[*this] 来实际复制对象。
  • @HTNW 我重写了一些答案。有点令人困惑的是,我实际上认为[this] 捕获了this 指针通过引用,根据[expr.prim.lambda.capture]/10 和/12。并不是说我认为 this 指针本身的捕获方式(通过引用值)会有什么不同,因为this 始终是一个纯右值(这也应该适用于在函数调用运算符中使用它时)闭包类型),因此显式 [this] 捕获的语义确实是您所期望的。
  • 简单捕获this 不通过引用捕获指针。 [expr.prim.lambda.capture]/4: "simple-capturethis* this 表示本地实体 *this。" *this 分别通过引用或复制捕获,而不是指针。
猜你喜欢
  • 2023-03-16
  • 2010-11-06
  • 1970-01-01
  • 1970-01-01
  • 2014-06-07
  • 2022-01-21
  • 2012-03-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多