[boost.async] Dummy co_return from a generator
Hi Everyone. I am very uneasy about the dummy co_retuned value that I see in the examples of async::generator. Like the one in the echo server example: https://klemens.dev/async/tutorial.html#echo_server async::generator<tcp_socket> listen() { tcp_acceptor acceptor({co_await async::this_coro::executor}, {tcp::v4(), 55555}); for (;;) { tcp_socket sock = co_await acceptor.async_accept(); co_yield std::move(sock); } co_return tcp_socket{acceptor.get_executor()}; // :-( } The control will never get to the co_return. The caller never even tries to observe this value. and yet we are forced to return it. Interestingly, the coroutine example from ASIO doesn't have this due to a different design: https://www.boost.org/doc/libs/1_83_0/doc/html/boost_asio/example/cpp20/coro... std::generator doesn't have this. In other examples the degenerate value is used to indicate the end of generation. I do not know the coroutines to be able to tell if this is a design problem with async::generator or with C++ coroutines in general. But it feels wrong that the end of generation should be signaled in this way. Regards, &rzej;
On Sun, Sep 24, 2023, 3:51 AM Andrzej Krzemienski via Boost < boost@lists.boost.org> wrote:
Hi Everyone. I am very uneasy about the dummy co_retuned value that I see in the examples of async::generator. Like the one in the echo server example: https://klemens.dev/async/tutorial.html#echo_server
async::generator<tcp_socket> listen() { tcp_acceptor acceptor({co_await async::this_coro::executor}, {tcp::v4(), 55555}); for (;;) { tcp_socket sock = co_await acceptor.async_accept(); co_yield std::move(sock); } co_return tcp_socket{acceptor.get_executor()}; // :-( }
The control will never get to the co_return. The caller never even tries to observe this value. and yet we are forced to return it.
You can throw a dummy exception, too. Never gets executed anyhow.
Interestingly, the coroutine example from ASIO doesn't have this due to a different design:
https://www.boost.org/doc/libs/1_83_0/doc/html/boost_asio/example/cpp20/coro...
std::generator doesn't have this.
In other examples the degenerate value is used to indicate the end of generation.
I do not know the coroutines to be able to tell if this is a design problem with async::generator or with C++ coroutines in general. But it feels wrong
Why? How would you want to communicate to the awaiter that the coro is done? that the end of generation should be signaled in this way.
Regards, &rzej;
_______________________________________________ Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost
niedz., 24 wrz 2023 o 02:39 Klemens Morgenstern < klemensdavidmorgenstern@gmail.com> napisał(a):
On Sun, Sep 24, 2023, 3:51 AM Andrzej Krzemienski via Boost < boost@lists.boost.org> wrote:
Hi Everyone. I am very uneasy about the dummy co_retuned value that I see in the examples of async::generator. Like the one in the echo server example: https://klemens.dev/async/tutorial.html#echo_server
async::generator<tcp_socket> listen() { tcp_acceptor acceptor({co_await async::this_coro::executor}, {tcp::v4(), 55555}); for (;;) { tcp_socket sock = co_await acceptor.async_accept(); co_yield std::move(sock); } co_return tcp_socket{acceptor.get_executor()}; // :-( }
The control will never get to the co_return. The caller never even tries to observe this value. and yet we are forced to return it.
You can throw a dummy exception, too. Never gets executed anyhow.
Interestingly, the coroutine example from ASIO doesn't have this due to a different design:
https://www.boost.org/doc/libs/1_83_0/doc/html/boost_asio/example/cpp20/coro...
std::generator doesn't have this.
In other examples the degenerate value is used to indicate the end of generation.
I do not know the coroutines to be able to tell if this is a design problem with async::generator or with C++ coroutines in general. But it feels wrong
Why? How would you want to communicate to the awaiter that the coro is done?
First, there are a bunch of use cases where the consumer of the generator doesn't need to know, like the one with the listener: keep generating until you are canceled. Second, std::generator somehow does it. (I do not know how.) Back to your question, when I see a code structure like this: async::generator<T> fun() { while(cond) { co_yield something; } } I know that there is nothing to do after the last co_yield in the loop. So maybe the compiler/library should also. Modulo that this may not be doable in the library, or the hacks are too expensive. Regards, &rzej;
that the end of generation should be signaled in this way.
Regards, &rzej;
_______________________________________________ Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost
Why? How would you want to communicate to the awaiter that the coro is done?
First, there are a bunch of use cases where the consumer of the generator doesn't need to know, like the one with the listener: keep generating until you are canceled.
Ok, so it's technically UB, but you can skip the co_return. That will however generate a warning on msvc, so it's not officially recommended or mentioned in the docs. But I tested it on all compilers and it seemed to work. This is an issue with the C++ API. A coroutine promise can either have a return_value OR a return_void. I cannot have both at the same time. If that was possible, I'd do it.
Second, std::generator somehow does it. (I do not know how.)
It does it by using iterators. I.e. you advance the iterator, which will resume the coroutine. Then you check against end() if it co_returned (void) and then you get the cached value. The async::generator does it in one call.
Back to your question, when I see a code structure like this:
async::generator<T> fun() { while(cond) { co_yield something; } }
I know that there is nothing to do after the last co_yield in the loop. So maybe the compiler/library should also. Modulo that this may not be doable in the library, or the hacks are too expensive.
It's doable, but the price is more API complication. I think a dummy return while annoying is the best solution. Because otherwise the user needs to use a different generator type if he wants to use a significant value return as indication he's done (e.g. a generator<system::result<size_t>>.). The asio::experimental::coro defaults to using an optional btw., which I find much more cumbersome as a default, especially with the example above.
Regards, &rzej;
that the end of generation should be signaled in this way.
Regards, &rzej;
_______________________________________________ Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost
niedz., 24 wrz 2023 o 16:17 Klemens Morgenstern < klemensdavidmorgenstern@gmail.com> napisał(a):
Why? How would you want to communicate to the awaiter that the coro is
done?
First, there are a bunch of use cases where the consumer of the generator doesn't need to know, like the one with the listener: keep generating until you are canceled.
Ok, so it's technically UB, but you can skip the co_return. That will however generate a warning on msvc, so it's not officially recommended or mentioned in the docs. But I tested it on all compilers and it seemed to work.
It is only UB if the control reaches the end of the body. But if I know the loop is infinite and I will be always canceling the coroutine (calling .destroy()), reaching the end of function body does not happen, and therefore no UB.
This is an issue with the C++ API. A coroutine promise can either have a return_value OR a return_void. I cannot have both at the same time. If that was possible, I'd do it.
OK, I now understand why std::generator does not trigger an analogous warning in MSVC. std::generator<T>::promise_type defines the pair yield_value() and return_void(). So you can yield a value from a std::generator but you cannot return one. Question: Is it important to the design of async::generator to allow `co_return value`? All the examples of coroutines I have ever seen yield values in a loop.
Second, std::generator somehow does it. (I do not know how.)
It does it by using iterators. I.e. you advance the iterator, which will resume the coroutine. Then you check against end() if it co_returned (void) and then you get the cached value.
The async::generator does it in one call.
Yeah, so the interface of std::generator is similar to returning optional<T> form async::generator. In the sense that it returns two pieces of information: (1) whether we are at the end, (2) and if not, what value we have.
Back to your question, when I see a code structure like this:
async::generator<T> fun() { while(cond) { co_yield something; } }
I know that there is nothing to do after the last co_yield in the loop. So maybe the compiler/library should also. Modulo that this may not be doable in the library, or the hacks are too expensive.
It's doable, but the price is more API complication. I think a dummy return while annoying is the best solution. Because otherwise the user needs to use a different generator type if he wants to use a significant value return as indication he's done (e.g. a generator<system::result<size_t>>.).
Given the present interface, I have two ways of checking the "I am done" state: 1. One is to call generator::operator bool() 2. The other is to inspect the state of the yielded value. I see no use for the first one. There is an `operator bool` that doesn't do the job. Or did I misunderstand again? Would it be correct to say that I cannot use `async::generator<T>` effectively when my type `T` doesn't have a special dummy state?
The asio::experimental::coro defaults to using an optional btw., which I find much more cumbersome as a default, especially with the example above.
I am aware of two use cases. One where the resumer decides when the generation ends, the other when it is the generator that decides. For the former case, you are right. For the latter case, using optional is no worse than testing the dummy value on one side, and putting an additional code to generate the dummy value on the other. Regards, &rzej;
Regards, &rzej;
that the end of generation should be signaled in this way.
Regards, &rzej;
_______________________________________________ Unsubscribe & other changes:
On Sun, Sep 24, 2023 at 11:24 PM Andrzej Krzemienski <akrzemi1@gmail.com> wrote:
niedz., 24 wrz 2023 o 16:17 Klemens Morgenstern <klemensdavidmorgenstern@gmail.com> napisał(a):
Why? How would you want to communicate to the awaiter that the coro is done?
First, there are a bunch of use cases where the consumer of the generator doesn't need to know, like the one with the listener: keep generating until you are canceled.
Ok, so it's technically UB, but you can skip the co_return. That will however generate a warning on msvc, so it's not officially recommended or mentioned in the docs. But I tested it on all compilers and it seemed to work.
It is only UB if the control reaches the end of the body. But if I know the loop is infinite and I will be always canceling the coroutine (calling .destroy()), reaching the end of function body does not happen, and therefore no UB.
This is an issue with the C++ API. A coroutine promise can either have a return_value OR a return_void. I cannot have both at the same time. If that was possible, I'd do it.
OK, I now understand why std::generator does not trigger an analogous warning in MSVC. std::generator<T>::promise_type defines the pair yield_value() and return_void(). So you can yield a value from a std::generator but you cannot return one.
Question: Is it important to the design of async::generator to allow `co_return value`? All the examples of coroutines I have ever seen yield values in a loop.
I think so, especially for users that do not want to use exceptions for errors. So being able to co_return an error instead of co_yield-ing a value (e.g. using system::result) seems quite useful
Second, std::generator somehow does it. (I do not know how.)
It does it by using iterators. I.e. you advance the iterator, which will resume the coroutine. Then you check against end() if it co_returned (void) and then you get the cached value.
The async::generator does it in one call.
Yeah, so the interface of std::generator is similar to returning optional<T> form async::generator. In the sense that it returns two pieces of information: (1) whether we are at the end, (2) and if not, what value we have.
Back to your question, when I see a code structure like this:
async::generator<T> fun() { while(cond) { co_yield something; } }
I know that there is nothing to do after the last co_yield in the loop. So maybe the compiler/library should also. Modulo that this may not be doable in the library, or the hacks are too expensive.
It's doable, but the price is more API complication. I think a dummy return while annoying is the best solution. Because otherwise the user needs to use a different generator type if he wants to use a significant value return as indication he's done (e.g. a generator<system::result<size_t>>.).
Given the present interface, I have two ways of checking the "I am done" state: 1. One is to call generator::operator bool() 2. The other is to inspect the state of the yielded value.
I see no use for the first one. There is an `operator bool` that doesn't do the job. Or did I misunderstand again?
It'll tell you if it co_returned. So it's useful.
Would it be correct to say that I cannot use `async::generator<T>` effectively when my type `T` doesn't have a special dummy state?
No. Throwing an exception or skipping the co_return (ignoring the MSVC warning) would solve that issue.
The asio::experimental::coro defaults to using an optional btw., which I find much more cumbersome as a default, especially with the example above.
I am aware of two use cases. One where the resumer decides when the generation ends, the other when it is the generator that decides. For the former case, you are right. For the latter case, using optional is no worse than testing the dummy value on one side, and putting an additional code to generate the dummy value on the other.
Sure, but from the API perspectice, you can just use a generator<std::optional<T>> that co_yields T and co_returns std::nullopt. I don't see the issue with an additional dummy co_return at the end.
participants (2)
-
Andrzej Krzemienski
-
Klemens Morgenstern