Date: Thu, 14 May 2020 15:30:32 +0200
czw., 14 maj 2020 o 14:58 Avi Kivity via Std-Proposals
<std-proposals_at_[hidden]> napisaĆ(a):
>
>
> On 14/05/2020 15.09, Ville Voutilainen wrote:
>
> On Thu, 14 May 2020 at 14:59, Avi Kivity <avi_at_[hidden]> wrote:
>
> On 5/14/20 2:46 PM, Ville Voutilainen wrote:
>
> On Thu, 14 May 2020 at 14:34, Avi Kivity <avi_at_[hidden]> 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.
>
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
And code like:
```
lazy<int> get_custom_lambda() {
struct X
{
int i = 3;
lazy<int> operator()(){ co_return i; }
} x();
return x();
}
```
This still crash, and this is same code as `get_real_lambda`.
How do you like fix it? and what if lambda is not movable?
<std-proposals_at_[hidden]> napisaĆ(a):
>
>
> On 14/05/2020 15.09, Ville Voutilainen wrote:
>
> On Thu, 14 May 2020 at 14:59, Avi Kivity <avi_at_[hidden]> wrote:
>
> On 5/14/20 2:46 PM, Ville Voutilainen wrote:
>
> On Thu, 14 May 2020 at 14:34, Avi Kivity <avi_at_[hidden]> 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.
>
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
And code like:
```
lazy<int> get_custom_lambda() {
struct X
{
int i = 3;
lazy<int> operator()(){ co_return i; }
} x();
return x();
}
```
This still crash, and this is same code as `get_real_lambda`.
How do you like fix it? and what if lambda is not movable?
Received on 2020-05-14 08:33:47