[boost] [boost.async] Dummy co_return from a generator

29 views
Skip to first unread message

Andrzej Krzemienski via Boost

unread,
Sep 23, 2023, 3:51:41 PM9/23/23
to Boost mailing list, Andrzej Krzemienski
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/coroutines/echo_server.cpp

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;

_______________________________________________
Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost

Klemens Morgenstern via Boost

unread,
Sep 23, 2023, 8:39:38 PM9/23/23
to Boost mailing list, Klemens Morgenstern
On Sun, Sep 24, 2023, 3:51 AM Andrzej Krzemienski via Boost <
bo...@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/coroutines/echo_server.cpp
>
>
>
> 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?

Andrzej Krzemienski via Boost

unread,
Sep 24, 2023, 4:06:50 AM9/24/23
to Klemens Morgenstern, Andrzej Krzemienski, Boost mailing list
niedz., 24 wrz 2023 o 02:39 Klemens Morgenstern <
klemensdavi...@gmail.com> napisał(a):

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;

Klemens Morgenstern via Boost

unread,
Sep 24, 2023, 10:17:54 AM9/24/23
to Andrzej Krzemienski, Klemens Morgenstern, Boost mailing list
>>
>> 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.

Andrzej Krzemienski via Boost

unread,
Sep 24, 2023, 11:24:20 AM9/24/23
to Klemens Morgenstern, Andrzej Krzemienski, Boost mailing list
niedz., 24 wrz 2023 o 16:17 Klemens Morgenstern <
klemensdavi...@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;

Klemens Morgenstern via Boost

unread,
Sep 24, 2023, 11:37:42 AM9/24/23
to Andrzej Krzemienski, Klemens Morgenstern, Boost mailing list
On Sun, Sep 24, 2023 at 11:24 PM Andrzej Krzemienski <akrz...@gmail.com> wrote:
>
>
>
> niedz., 24 wrz 2023 o 16:17 Klemens Morgenstern <klemensdavi...@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.

Reply all
Reply to author
Forward
0 new messages