On 14/05/2020 15.09, Ville Voutilainen wrote:
On Thu, 14 May 2020 at 14:59, Avi Kivity <avi@scylladb.com> wrote:
On 5/14/20 2:46 PM, Ville Voutilainen wrote:
On Thu, 14 May 2020 at 14:34, Avi Kivity <avi@scylladb.com> wrote:
With a lambda coroutine, we have a problem. This is because lambdas are
captured by reference:
Says what?
Says the quote below, from cppreference.
Your wording was confusing; I thought you meant a lambda as a
coroutine parameter.
For cases where we are in a non-static member function, the *this is
indeed not copied.

Ah, the lambda _is_ a coroutine parameter in a sense, if you follow the
regular translation of a lambda to a struct with operator().


And note this is not specific to lambdas, it happens for any member
coroutine called on a temporary. A lambda is just the most common way of
generating such calls.
Right. Calling a member function coroutine of a temporary seems like
madness to me.
It's quite like calling a member function that queues async work that
is later completed in
some other member of the object.


It is super common in async frameworks.


For example the predecessor of coroutines is future::then, which accepts a callable that is called when the future becomes ready. So if I write

  

   future<int> get_int();


   int foo;

   extern int bar;

   get_int().then([foo] (int val) -> future<> {

        int another = co_await get_int();

        bar = foo + val + another;

   });


This breaks with the current standard, because the lambda object can still be on the stack when operator()(int) is invoked.



I'm not sure I understand where the temporary really is in the
not-so-lame testcase in the bug report. To me,
it seems like everything is stored and lifetimes don't end
prematurely, but I don't understand how initial_suspend
really works. :)


Copying it here for reference:

#include <coroutine>
#include <functional>
#include <optional>
#include <cassert>

template <typename T>
class lazy {
    std::function<T ()> _compute;
    lazy(std::function<T ()> compute) : _compute(std::move(compute)) {}
public:
    T get() { return _compute(); }
    static lazy make(std::function<T ()> compute) {
        return lazy(std::move(compute));
    }
};

namespace std {

template <typename T, typename... Args>
struct coroutine_traits<lazy<T>, Args...> {
    struct promise_type {
        std::optional<T> value;
        suspend_always initial_suspend() const { return {}; }
        suspend_always final_suspend() const { return {}; }
        void return_value(T val) {
            value = std::move(val);
        }
        lazy<T> get_return_object() {
            return lazy<T>::make([this] () -> T {
                auto handle = coroutine_handle<promise_type>::from_promise(*this);
                handle.resume();
                auto ret = std::move(*value);
                handle.destroy();
                return ret;
            });
        }
        void unhandled_exception() {
            std::terminate();
        }
    };
};

}

struct fake_lambda_state {
    int i = 5;
};

lazy<int> fake_lambda(fake_lambda_state s) {
    co_return s.i;
}

lazy<int> get_fake_lambda() {
    // fake_lambda_state() is captured in the promise
    return fake_lambda(fake_lambda_state());
}

lazy<int> get_real_lambda() {
    // the state (i) is not captured
    return [i = 6] () -> lazy<int> {
        co_return i;
    }();
}

int main(int ac, char** av) {
    auto l1 = get_fake_lambda();
    assert(l1.get() == 5);
    auto l2 = get_real_lambda();
    assert(l2.get() == 6);
    return 0;
}


The following happens in the execution of "auto l2 = get_real_lambda()". Please forgive any excessive detail, I figure it is better to provide too much than too little:


1. A lambda object is constructed on the stack, with i initialized to 6.

2. The lambda's operator()() is called.

3. A coroutine frame is allocated with space for a pointer to the lambda.

4. The pointer to the lambda is copied to the coroutine frame.

5. initial_suspend() is called, and because it returns suspend_always, get_return_object() is called.

6. get_return_object returns a lazy<int> initialized with some std::function, that captures the coroutine handle (really the frame)

7. the coroutine returns the its caller, get_real_lambda()

8. get_real_lambda destroys the lambda (creating a dangling reference)

9. the lazy<int> is assigned to l2.


The next line, l2.get():


10. lazy<int>::get() is called, calling the function we created in step 6

11. The coroutine handle is used to resume the coroutine just after the point where we called initial_suspend (at the "{")

12. We evaluate "i" for "co_return i". This is really lambda_ptr->i, where lambda_ptr was captured in step 4.

13. the access to i is to a destroyed stack frame, which is undefined behavior, so the compiler is at liberty to destroy all life on earth.


Note that fake_lambda conceptually does exactly the same things, but because it is captured by value, everything works.