Fun with “deducing this
” lambdas
this
” lambdasSomeone on the cpplang Slack asks: Why can I give
a non-capturing lambda a C++23 “deducing this
” explicit object parameter of an
arbitrary type, but I can’t do the same for a capturing lambda?
auto alpha = [](this std::any a) { return 42; };
// OK
auto beta = [&](this std::any a) { return 42; };
// ill-formed
GCC complains: “a lambda with captures may not have an explicit object parameter of an unrelated type.” (GCC’s diagnostic is — I think properly — SFINAE-friendly. Clang and MSVC — I think improperly — allow you to form the lambda type, and then error on any attempt to call it.)
This restriction exists by the following logic: A capturing lambda has captures, presumably,
because it wants to use them. To use its captures, the lambda’s operator()
must have access
to the lambda object (because its captures are stored as data members of that object). Therefore,
the operator()
must have an object parameter of the lambda’s own type — or at least a type
unambiguously derived from that type!
auto gamma = [x]() { return x + 1; };
is lowered by the compiler into basically
struct Gamma {
int x_;
auto operator()() const { return x_ + 1; }
};
auto gamma = Gamma{x};
It’s able to get at x_
(i.e. this->x_
) only because it has a this
parameter
(an “implicit object parameter”) of type Gamma
. Change the lambda to use either
of the new C++23 syntaxes which fiddle with that object parameter, and you’ll find
trouble. The static
specifier removes the object parameter entirely:
auto delta = [x]() static { return x + 1; };
// is ill-formed...
struct Delta {
int x_;
static auto operator()() { return x_ + 1; }
// ...because this is ill-formed!
};
The this
specifier fiddles with the object parameter’s type, either by pinning
it down to one concrete type, or by making it a template parameter:
auto zeta = [x](this std::any self) { return x + 1; };
// is ill-formed...
struct Zeta {
int x_;
auto operator()(this std::any self) {
return static_cast<Zeta&>(self).x_ + 1;
// ...because this is ill-formed!
}
};
auto eta = [x](this auto self) { return x + 1; };
// can be ill-formed...
struct Eta {
int x_;
auto operator()(this auto self) {
return static_cast<Eta&>(self).x_ + 1;
// ...(roughly) whenever this is ill-formed!
}
};
But the lowering is not the reality!
In the above examples, I was careful to use the name x
for the lambda’s capture (in the real C++23 code)
and the name x_
for the struct’s data member (in the lowered code). That reminds us that the lowering
operation isn’t a program transformation; you can’t blithely assume that everything inside
the lambda-expression’s curly braces works exactly the same as it would in an ordinary member function.
For example, the keyword this
acts “differently” inside a lambda — which is to say, it acts the same
as it does outside the lambda. That’s usually what we want.
struct Worker {
void run();
void sync_run_on_this_thread() {
this->run(); // OK
}
void sync_run_on_another_thread() {
std::thread([&]() {
this->run(); // OK
}).join();
}
};
It’s convenient that the two lines marked OK
both mean the same thing. Inside a lambda, historically,
there’s been no sense in letting this->run()
mean “invoke the run
method of this lambda object itself,”
because that’s never been something you can do with a lambda. Starting in C++23, we can create types derived
from lambda types whose operator()
(thanks to “deducing this
”) can actually get at the derived type.
So we can now create puzzles like this…
return [&](this auto self) {
printf("%d %d %d\n", x, this->x, self.x);
};
When embedded at the line marked HERE
in this program, the lambda above prints “1 2 3”
(Godbolt) — that is, each name x
refers to a different
object.
int one = 1;
struct Enclosing {
int x = 2;
auto factory(int& x = one) {
// HERE
}
};
using Base = decltype(Enclosing().factory());
struct Derived : Base {
int x = 3;
Derived(Enclosing& e) : Base(e.factory()) {}
};
int main() {
auto e = Enclosing();
auto theta = Derived(e);
theta();
}
Of course, this assumes you find a C++23 compiler capable of compiling this program at all! Readers may recall Knuth’s “man or boy” test for ALGOL 60; this program seems to be a bit like that for C++23 at the moment.