Search code examples
c++lambdac++23

Restriction of explicit object parameter in lambda with capture


In C++23, lambda-expression supports an explicit object parameter (a.k.a. "Deducing this"). I found strange restriction for lambda with capturing at [expr.prim.lambda]/p5.

Given a lambda with a lambda-capture, the type of the explicit object parameter, if any, of the lambda's function call operator (possibly instantiated from a function call operator template) shall be either:

  • the closure type,
  • a class type derived from the closure type, or
  • a reference to a possibly cv-qualified such type.

[Example 2:

struct C {
  template <typename T>
  C(T);
};

void func(int i) {
  int x = [=](this auto&&) { return i; }();  // OK
  int y = [=](this C) { return i; }();       // error
  int z = [](this C) { return 42; }();       // OK
}

-- end example]

Question: Why is there such restriction for lambda with capture only? Are there any implementation issues?


Solution

  • There's a few things at play here.

    First, lambda captures have no names. So if we just take a regular old C++11 lambda:

    void foo(int i) {
        auto f = [=](){ return i; };
    }
    

    f there captures i, but there is no member f.i. It's just that naming i from within the body of the lambda magically refers to that capture. There's no way to explicitly name it within the body either - within a class type you could write i or this->i, but there's no latter option for a lambda (since any use of this would refer to some outer captured this instead).

    This lack of ability to name the lambda itself was one of the motivations for the deducing this feature.

    Now, we can write that lambda differently:

    auto g = [=](this auto self){
        return self.i; // ??
    };
    

    Here, self is actually the lambda itself, when you call it (the expression g() would copy g into the self parameter). And this has to be a generic lambda, because lambdas have unnameable types - you can't write decltype(g) there. But... g is still a lambda, and lambda captures have no names. And we didn't want to suddenly add those names, so we end up with this:

    auto g2 = [=](this auto self){
        return i; // ?!?
    };
    

    This is, admittedly, pretty weird right? i here behaves the same way as i in the original f lambda - it's transformed into an access of the lambda capture. But which lambda? self.

    So now because the only way to access a lambda capture in a lambda with an explicit object parameter is implicit, we need to ensure that the explicit object parameter actually is the lambda that we're currently defining - otherwise the code is kind of nonsensical. We captured i by copy into the lambda, but we don't have the lambda in scope to get i from, so what can we even do?

    Hence the cited rule: if there is a lambda capture and an explicit object parameter, we need to ensure that the explicit object parameter is actually the lambda doing the capturing (or a type derived from it).

    Had lambda captures been named (either initially or introduced in the C++23 along with deducing this in some form or other), then this rule wouldn't have been necessary since you'd just write return self.i if that's what you wanted. But this then presents a different kind of problem: how do you know what [=] or [&] actually... captures? So the oddity that implicitly naming captures works, even in the context of explicit object parameters in lambdas, is probably the least strange option in this space.