可变 lambda 表达式中能否更改 `this`?

17

每个人都知道在C++中this对象指针不能在方法中改变。但是对于可变的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 被复制捕获,所以更改它不会更改原始指针。这应该总是被允许的。在第二种情况下,this 被引用捕获,因为 [this] 是一个特殊情况。 - VLL
1个回答

15

this 的初始化捕获

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

这里使用了init-capture来以值的方式捕获this指针,根据[expr.prim.lambda.capture]/6的意思,它是this指针的一个副本。在thisconst修饰的情况下,即使lambda是可变的(与'point to const'相比),该副本自然不能用于更改this,但是由于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的大小写:
  • capture-list包含一个capture
  • capturesimple-captureinit-capture之一;我们忽略后者,因为它已经在上面讨论过了
  • simple-capture有以下形式之一:
    • identifier ...可选
    • &identifier ...可选
    • this
    • *this
根据[expr.prim.lambda.capture]/10 [强调是我的]:

如果实体被隐式捕获,捕获方式为拷贝,则:

  • (10.1) 捕获默认值为=,并且被捕获的实体不是*this;或者

  • (10.2) 它被显式捕获,并且捕获方式不是this&identifier,或&identifier initializer

simple-capture形式的*this允许通过拷贝显式捕获*this对象。然而,根据[expr.prim.lambda.capture]/12simple-capture形式的this通过引用捕获*this对象:

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

如果实体被显式或隐式捕获但未被拷贝捕获,则它被引用捕获。对于通过引用捕获的实体,闭包类型是否声明了其他未命名非静态数据成员是未指定的。[...]

因此:
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不是可修改的lvalue,这就是为什么#2无效的原因:
在非静态成员函数的函数体中,关键字this是一个prvalue,其值是调用该函数的对象的指针。在类型具有cv限定符序列cv并且类为X的成员函数中,this的类型为“指向cv X的指针”。 [...]
根据[expr.prim.lambda.closure]/12,当在lambda表达式中使用this时也适用于此: lambda表达式的复合语句产生函数调用运算符的函数体,但是对于名称查找、确定this的类型和值以及将引用非静态类成员的id表达式转换为类成员访问表达式(使用* this)[class.mfct.non-static],复合语句在lambda表达式的上下文中被认为。
MSVC接受你的片段(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限定符序列cv且所属类为X的成员函数中,this的类型应该是“指向cv X”的指针” [...]

...而不是“指向cv X的常量指针”(prvalue的constness本来就很奇怪)。


@HTNW 我重新修改了我的回答。有点令人困惑的是,根据[expr.prim.lambda.capture]/10和/12,我实际上认为[this]通过引用捕获了this指针。并不是我认为这会对this指针本身如何被捕获(按值或引用)产生任何影响,因为this始终是一个prvalue(当它在闭包类型的函数调用运算符中使用时也应该如此),因此显式[this]捕获的语义确实是您所期望的。 - dfrib
@bogdan 你说得对,谢谢(我还错误地引用了/7.2来说明this也表示本地对象*this,但/4确实是正确的参考)。 - dfrib
[class.this]/1 不能在 lambda 内部使用,因为这会使 [*this](){ std::cout << this; } 引用原始对象,而我们知道它实际上是引用复制捕获。 - Ben Voigt
@dfrib:是的,有了那个限制“用于确定类型和值”,它又变得有意义了。如果引用那个闭包/12,我认为答案会更清晰。 - Ben Voigt
@BenVoigt 我更新了一个涉及到[闭包]/12的参考文献。 - dfrib
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接