Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can `this` be changed in a mutable lambda?

Everyone knows that this object pointer in C++ cannot be changed in its methods. But what comes to mutable lambdas, where this is captured, some current compilers give such possibility. Consider this code:

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
        };
    }
};

In the first lambda this is captured and given a new name p. Here all compilers permit to change the value of p. In the second lambda this is captured by its own name, and only MSVC allows the programmer to change its value. Demo: https://gcc.godbolt.org/z/x5P81TT4r

I believe MSVC behaves incorrectly in the second case (though it looks like a nice language extension). Could anyone find the right wording from the standard (the search is not easy since the word this is mentioned there 2800+ times)?

like image 506
Fedor Avatar asked Aug 17 '21 06:08

Fedor


1 Answers

Init-captures of this

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

This uses init-capture to capture the this pointer by value, as per [expr.prim.lambda.capture]/6, meanings it's a copy of the this pointer. In contexts where this is const-qualified, the copy can naturally not be used to changed this (even if the lambda is mutable; compare with 'point to const'), but as the lambda is mutable, the pointer (copy) can be used to point to something different, e.g. 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{};
};

Simple-captures of this:

(void) [this]() mutable { 
    this = nullptr; //#2: ok in MSVC only
};

As per [expr.prim.lambda.capture], ignoring the case of capture-default:s:

  • A capture-list contains a capture
  • A capture is either a simple-capture or an init-capture; we ignore the latter case as it was covered above
  • A simply-capture has one of the following forms:
    • identifier ...opt
    • &identifier ...opt
    • this
    • *this

As per [expr.prim.lambda.capture]/10 [emphasis mine]:

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.

Only the simple-capture form *this allows explicitly capturing the *this object by copy. The simple capture this, however, captures the *this object(+) by reference, as per [expr.prim.lambda.capture]/12:

(+) The simple-capture:s this and *this both denote the local entity *this, as per [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. [...]

Thus:

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{};
};

As per [class.this]/1, this is not a modifyable lvalue, which is why #2 is ill-formed:

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”. [...]

Which, as per [expr.prim.lambda.closure]/12, applies also to when this is used in lambdas:

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 is this incorrect to accept your snippet (accepts-invalid).

And indeed, in the following example (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();
}

We expect the second g overload to be a better match, as this is a prvalue. However, whilst GCC and Clang behaves as expected:

// GCC & Clang: rvalue pointer to S

MSVC fails to even compile the program:

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

Which is in violation of [class.this]/1, as:

[...] 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” [...]

... and not "const pointer to cv X" (constness on a prvalue would be weird, to begin with).

like image 199
dfrib Avatar answered Oct 25 '22 17:10

dfrib