C++ Logo

std-proposals

Advanced search

Re: Coroutine local destructor

From: Marcin Jaczewski <marcinjaczewski86_at_[hidden]>
Date: Sun, 24 May 2020 14:45:14 +0200
niedz., 24 maj 2020 o 06:11 Lewis Baker <lbaker_at_[hidden]> napisał(a):
>
> The reason that the promise_type::operator delete() is not invoked with
> the arguments is because those arguments would have already been destructed
> by the time the 'operator delete' function is called.
>
> When the 'operator new' is called to allocate the coroutine frame, the arguments
> that are passed to it are references to the actual arguments to the coroutine
> function. These arguments are guaranteed to still be alive at this point as we
> are still inside the execution of the coroutine function.
> This allows us to "preview" those parameters when allocating the coroutine-frame.
>
> After allocating the coroutine frame, those parameters are then copied/moved
> into the coroutine-frame before then constructing the promise object.
> The promise object can also preview the arguments, but in this case it is
> passed references to the parameter copies instead of the original arguments.
>
> Assuming coroutine suspends at some point and returns from the original call
> to the coroutine then the original arguments will have been destroyed,
> leaving the coroutine with only access to the copies it made in the
> coroutine frame.
>
>
> When the coroutine-frame is destroyed (either by implicitly running off the
> end of the coroutine - not suspending at final_suspend(), or by calling
> .destroy() on the coroutine_handle) the in-scope locals are destroyed, then
> the promise is destroyed and then the parameter copies are destroyed.
> This leaves the coroutine-frame now with uninitialized memory that is safe
> for the deallocation function to free.
>
>
> If you want to have access to any of the arguments in the `operator delete`
> implementation then you will need to allocate some additional storage within
> the memory allocated by 'operator new' and store a copy of the argument(s)
> in that additional storage so that they are still available to
> 'operator delete' after the promise and parameter copies have already been
> destroyed.
>
> This is the strategy taken by the implementation of P1056R0 that written
> for libc++. See https://reviews.llvm.org/D46140. This allows customization
> of the allocator by passing std::allocator_arg and an allocator as the first
> two arguments of a task coroutine.
>

But argument passed to destructor need have same address as one
available in corutine body?
We already once copy them once, why not move them on start of `destroy`?
This mean we will have 3 places where they values would live:
a) Real function arguments
b) corutine frame
c) local variables in `destroy` that are passed to `operator delete`

This last part could look like:
```
void __create(A1 a1, A2 a2, A3 a3)
{
    frame = operator new(sizeof(Frame), a1, a2, a3);
    frame->args = { std::move(a1), std::move(a2), std::move(a3) };
}

void destroy()
{
    auto args = std::move(frame->args);
    frame->~Frame();
    operator delete(frame, std::move(args.a1), std::move(args.a2),
std::move(args.a3));
}
```

Of corse this code will have some overhead but you need output it
only when end user request additional parameters for `operator
delete`.
Lifetime of referenced variables should not be problem, because if
there would be, then coruntie itself hand this problem after first
suspend.
Same with not movable types, if it was any, then you cannot call
`operator new` on it or cannot move it to corutine frame.


Basie even if this approach cannot be done, then we still should have
`std::destroying_delete_t` where we could have code like:
```
void* new(size_t s, A1& a1, A2 a2, A3 a3)
{
    return a1.allocator().allocate(s);
}

template<typename Frame> //some opaque type for end user
void operator(Frame* frame, std::destroying_delete_t)
{
    //manual access to args from frame
    A1& a1 = std::corutine_arg<0>(frame);
    A2 a2 = std::move(std::corutine_arg<1>(frame));
    A3 a3 = std::move(std::corutine_arg<2>(frame));
    task_promise& promise = std::corutine_promise(frame);
    auto allocator = a1.allocator();

    std::corutine_destructor(frame); //promise become invalid there

    allocator.deallocate(frame, sizeof(Frame));
}
```

>
> Cheers,
> Lewis
>
> On 5/22/20, 9:56 AM, "Std-Proposals on behalf of Marcin Jaczewski via Std-Proposals" <std-proposals-bounces_at_[hidden] on behalf of std-proposals_at_[hidden]> wrote:
>
> After some time playing around C++20 I find out that corutine can't
> have local aware destructor.
>
> code like this:
> ```
> task<> foo(Storage& t, Args args);
> ```
>
> Can use operator `new` like:
> ```
> void* operator new(std::size_t s, Storage& t, Args args);
> ```
>
> But destructor can only have two forms:
> ```
> void operator delete(void* p, std::size_t s);
> void operator delete(void* p);
> ```
>
> This mean if I would want store this corutine in `Storage` then I have
> now easy way to inform `Storage` to release memory for that corutine.
>
> Could be possible for C++ support destructor like:
> ```
> void operator delete(void* p, std::size_t s, Storage& t, Args args);
> ```
>
> or at least new in C++20 destructor:
> ```
> void operator delete(task_promise* p, std::destroying_delete_t);
> ```
>
> second options will have benefit of not needed defining how exactly
> args could survive to point of calling destructor as they should be
> placed on allocated space by corutine.
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://urldefense.proofpoint.com/v2/url?u=https-3A__lists.isocpp.org_mailman_listinfo.cgi_std-2Dproposals&d=DwICAg&c=5VD0RTtNlTh3ycd41b3MUw&r=92rQsBGSkJNIu4JBui5p1Q&m=Dq_rroTe3O1tUw9OwwQTruO0c6PdFZFcYdEKVdLT_dI&s=mblaPnhzX0Af-BFj3NoGrbAfMjlw1FBH6rISrsllzwE&e=
>

Received on 2020-05-24 07:48:27