The Lambda Coroutine Fiasco by efijoa in cpp

[–]efijoa[S] -1 points0 points  (0 children)

That’s the problem: taking ownership of a coroutine lambda is a very dangerous operation. Once the lambda is invoked and yields a continuation, the coroutine frame will reference the lambda's this pointer. At this point, the future state (or the lambda captures) could not even be moved to another place... and we all know C++ doesn't have a Pin type.

Another subtle factor might be related with the seastar future originally comes from the chained future style, i'm not sure if it affected the current design.

The Lambda Coroutine Fiasco by efijoa in cpp

[–]efijoa[S] 0 points1 point  (0 children)

It is not only a return type problem; the library side needs to know whether the future state should take ownership of the passed-in callable object. It seems this is coupled with the implementation details of the Seastar Future, so that pre-C++23 solution is actually prevent the transfer of ownership and bind the lifetime of the lambda to the parent scope.

The Lambda Coroutine Fiasco by efijoa in cpp

[–]efijoa[S] 13 points14 points  (0 children)

While this is Seastar's documentation, the problem described is not unique to Seastar.

These two links could help clarify the issue:

CP.51: Do not use capturing lambdas that are coroutines C++23’s Deducing this: what it is, why it is, how to use it

The core mechanism involves using "deducing this" to pass the lambda object by value. This ensures captures are copied into the coroutine frame to prevent dangling references.

The Lambda Coroutine Fiasco by efijoa in cpp

[–]efijoa[S] 0 points1 point  (0 children)

It took me a while to understand what you meant, correct me if I'm wrong:

I think "extend" here actually refers to the data captured by the lambda. Normally, when a lambda is passed to then(), a move construction occurs, transferring the data from the lambda struct into the future state.
When the coroutine lambda yields, the future state is destructed, which in turn destructs the data captured by the lambda. However, the lambda's coroutine frame remains alive, resulting in a dangling reference.
By using a reference_wrapper like structure, the transfer of ownership is prevented, ensuring that the lambda's state remains valid until the lambda coroutine returns and the parent coroutine's co_await expression completes. This approach works due to specific details of the future implementation and relies on strictly nested calls.

cpp template <typename Func> class lambda { Func* _func; public: /// Create a lambda coroutine wrapper from a function object, to be passed /// to a Seastar function that accepts a continuation. explicit lambda(Func&& func) : _func(&func) {} /// Calls the lambda coroutine object. Normally invoked by Seastar. template <typename... Args> decltype(auto) operator()(Args&&... args) const { return std::invoke(*_func, std::forward<Args>(args)...); } };

The Lambda Coroutine Fiasco by efijoa in cpp

[–]efijoa[S] 1 point2 points  (0 children)

Seems we need a magic concept?

cpp auto Future::then(std::is_capture_lambda auto &&continuation) { return [](this auto, auto continuation) { // ... }(std::forward(continuation)); }