Do temporaries passed to a function that returns awaitable remains valid after suspension point with co_await

帅比萌擦擦* 提交于 2021-01-28 02:24:25

问题


I'm adding support for coroutines ts in an async socket class based on windows io completion ports . Without coroutines the io may be done like this :

sock.async_write(io::buffer(somebuff), [](auto&& ... args){ /* in handler */ });

or

sock.async_write(std::vector<io::const_buffer>{ ... }, [](auto&& ... args){ /* in handler */ })

where each one will returns void and will notify the result through the handler and doesn't need to cache the parameters because the operation will have been submitted upon returning from the function

But with coroutines the function will return an awaitable that upon awaiting it with operator co_await will submit the operation so I need to cache the parameters in the awaitable to avoid using destructed temporaries :

awaitable coro_write(const io::const_buffer& buff)
{
    return awaitable{ *this, buff }; 
}

awaitable coro_write(const std::vector<io::const_buffer>& buffs)
{
    return awaitable{ *this, buffs };
}

the copy in the first one doesn't harm but in the second it does , cause it will trigger a heap allocation and copy the vector contents.

So I was searching for a solution to this and while reading this page coroutines ts I came across this :

A typical generator's yield_value would store (copy/move or just store the address of, since the argument's lifetime crosses the suspension point inside the co_await) its argument into the generator object and return std::suspend_always, transferring control to the caller/resumer.

and from the same page it is stated that co_yield expression is equivalent to :

co_await promise.yield_value(expr)

which is also similar to :

co_await sock.coro_write(expr)

I opened the generator header shipped with visual studio 2019 and saw that it also stored the address of the parameter to yield_value and retrieved it later through generator::iterator::operator *() in the caller site after the coroutine suspension :

struct promise_type {
    _Ty const* _CurrentValue;
     auto yield_value(_Ty const& _Value) {
         _CurrentValue = _STD addressof(_Value);
         return suspend_always{};
     }
}

struct iterator {
    _NODISCARD reference operator*() const {
        return *_Coro.promise()._CurrentValue;
    }
    _NODISCARD pointer operator->() const {
        return _Coro.promise()._CurrentValue;
    }
}

from this I concluded that the parameter passed to the function that returns an awaiter used with co_await will also remain valid until the coroutine is resumed or destoryed , is this right ? or this is special for yield_value in a promise type ?


回答1:


Tested with visual studio 2019 16.4.4 and it works . In fact the passed parameters and converted parameters also remains valid and doesn't get destructed until after await_resume returns or await_suspend doesn't suspend due to a thrown exception or a return value that indicates no intention to suspend the coroutine .

This is what I've done :

1 - created destructors for the buffers types as follows :

~const_buffer()
{
    std::cout << __FUNCSIG__ << std::endl;
}

~mutable_buffer()
{
    std::cout << __FUNCSIG__ << std::endl;
}

// mutable to const conversion

operator const_buffer() const noexcept { return { data_, size_ }; }

2 - at the end of issue_coro_op which is called from await_suspend I put similar print :

void issue_coro_op(awaitable& a)
{
    // use the awaitable to issue the op
    std::cout << "~ " << __FUNCSIG__ << std::endl;
}

3 - at the end of await_resume I put a similar print

4 - this is the type of buffers parameters passed :


awaitable coro_read(const io::mutable_buffer& buff, transfer_flags);

awaitable coro_write(const io::const_buffer& buff, transfer_flags);

template </**/>
awaitable::awaitable(const io::mutable_buffer& buff) { /*store in ptr*/ }

template </**/>
awaitale::awaitable(const io::const_buffer& buff) { /*store in ptr*/ }

5 - and this is the echo coroutine :

io::task<> run_session(tcp::async_socket sock)
{
    char buff[1024];
    string server_msg;
    try
    {
        for (;;)
        {
            auto n = co_await sock.coro_read(io::buffer(buff), transfer_flags::unspecified);
            if (buff[n - 1] == '\n')
                --n;
            cout << ">> " << string_view{ buff, n } << endl;

            server_msg = fmt::format("{{ server message : {} }}\n", string_view{ buff, n });
            n = co_await sock.coro_write(io::buffer(server_msg), transfer_flags::unspecified);
        }
    }
    catch (std::exception & ex)
    {
        cout << "[!] a client has disconnected : " << ex.what() << endl;
    }
}

6 - tested with nc :

nc localhost 4567
some message
{ server message : some message }

7 - server output :

issue_coro_op -> read
await_resume -> read
~mutable_buffer -> read
>> some message
issue_coro_op -> write
await_resume -> write
~const_buffer -> write
~mutable_buffer -> write

noting that io::buffer returns io::mutable_buffer which in write operations gets converted to io::const_buffer and the two remains valid until after resumption then they are destructed in the reverse order

I couldn't test with clang-cl 8 because it crashes while compiling the code ! and mingw-w64 with gcc 8 which doesn't support coroutines yet



来源:https://stackoverflow.com/questions/60737075/do-temporaries-passed-to-a-function-that-returns-awaitable-remains-valid-after-s

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!