I recently came across a situation where I ended up with a large number of nested lambdas to build asynchronous computation chains.
template <typename F>
struct node : F
{
node(F&& f) : F{std::move(f)}
{
}
template <typename FThen>
auto then(FThen&& f_then)
{
return ::node{[p = std::move(*this), t = std::move(f_then)]()
{
}};
}
};
int main()
{
auto f = node{[]{ }}.then([]{ }).then([]{ });
return sizeof(f);
}
All the objects I capture in my the lambdas are empty, yet the size of the final object is greater than one: example on gcc.godbolt.org.
If I change the lambda inside node</* ... */>::then
to a function object with explicit EBO, the size of the final object becomes one.
template <typename P, typename T>
struct node_lambda : P, T
{
node_lambda(P&& p, T&& t) : P{std::move(p)}, T{std::move(t)}
{
}
void operator()()
{
}
};
template <typename FThen>
auto node</* ... */>::then(FThen&& f_then)
{
return ::node{node_lambda{std::move(*this), std::move(f_then)}};
}
Live example on gcc.godbolt.org
I find this really annoying because I'm forced to either:
Write a lot of boilerplate code that is roughly equivalent to the lambda.
Pay an additional memory cost due to the fact that something like EBO doesn't apply to lambda captures.
Is there anything in the Standard that explicitly forces empty lambda captures to take additional space? If so, why?
From expr.prim.lambda.capture:
For each entity captured by copy, an unnamed non-static data member is declared in the closure type.
While the lambdas here have no capture:
auto f = node{[]{ }}.then([]{ }).then([]{ });
and hence have no unnamed non-static data members, and hence are empty, that's not what then()
actually uses. It uses this:
return ::node{[p = std::move(*this), t = std::move(f_then)](){}};
that lambda captures t
and p
by copy, and hence has two unnamed non-static data members. Each .then()
adds another member variable, even if each one is empty, hence the size of the node keeps going up.
Or in other words, the empty base optimization only applies to bases, and capture for lambdas doesn't create bases, it creates non-static data members.
Given the as-if rule and [expr.prim.lambda.closure]/2:
An implementation may define the closure type differently from what is described below provided this does not alter the observable behavior of the program other than by changing:
- the size and/or alignment of the closure type,
- whether the closure type is trivially copyable (Clause [class]),
- whether the closure type is a standard-layout class (Clause [class]), or
- whether the closure type is a POD class (Clause [class]).
I don't see anything preventing an implementation from using some kind of magic to optimize away the storage for the captured empty variable.
That said, doing so would be an ABI break, so don't hold your breath.
Allowing - or requiring - an implementation to make the type of a captured empty variable a base of the closure type, on the other hand, would be a horrendously bad idea. Consider:
struct X { };
struct Y { };
void meow(X x); // #1
void meow(Y y); // #2
void meow(std::function<void()> f); // #3
template<class T, class U>
void purr(T t, U u) {
meow([t = std::move(t), u = std::move(u)] { /* ... */ });
}
It would be insane for purr
to do anything other than call #3, yet if captures can become bases then it can call #1, or #2, or be ambiguous.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With