coroutines have odd interaction with std algorithms

155 views
Skip to first unread message

Jim Hurd

unread,
Dec 15, 2017, 10:02:09 AM12/15/17
to ISO C++ Standard - Future Proposals
Maybe this should have some kind of compiler warning, like a warning that the coroutine is being initialized but never resumed

generator<int> f2()
{
    auto g = { 1,2,3 };
    // this does work
    for (auto e : g) co_yield e*2;
    // this compiles fine, and might be expected to work, but doesn't
    // each iteration only initializes the coroutine, but doesn't yield anything
    // using a recursive_generator makes no difference
    std::for_each(g.begin(), g.end(), [](int x) {co_yield x * 2; });
    co_return;
}

Nicol Bolas

unread,
Dec 15, 2017, 11:31:00 AM12/15/17
to ISO C++ Standard - Future Proposals
I haven't looked in detail at the most recent version of the Coroutines TS. But what exactly is the deduced return type for `co_yield`? It can't be the `co_yield` expression's type, since the function has to return a future-like type.

Basically, I'm surprised that the lambda compiles at all.

Nicol Bolas

unread,
Dec 15, 2017, 11:46:52 AM12/15/17
to ISO C++ Standard - Future Proposals
According to N4649:

> A function declared with a return type that uses a placeholder type shall not be a coroutine

Since a lambda function without a declared return type uses a placeholder return type, it shall not be a coroutine. Therefore, the program is ill-formed.

Jim Hurd

unread,
Dec 15, 2017, 11:47:19 AM12/15/17
to ISO C++ Standard - Future Proposals
Good point, I'm not sure how generator<int> is picked to be the type. There can be any number of generators, so it seems rather bold to simply auto deduce generator<int>.

for whatever reason vc++ compiles like this:
    std::for_each(g.begin(), g.end(), [&](int x)->generator<int> {co_yield x * 2; });
of course as you suggest, you could specify another kind of generator
    std::for_each(g.begin(), g.end(), [&](int x)->recursive_generator<int> {co_yield x * 2; });

It still would compile and have the same somewhat counter-intuitive result.

Jim Hurd

unread,
Dec 15, 2017, 12:08:44 PM12/15/17
to ISO C++ Standard - Future Proposals

fwiw, Clang gets this right and flags the auto deduction as an error. I assume VC++ will catch up to that. This might be enough to warn the unwary. At least it makes it clear that a new generator is being created.

Alberto Barbati

unread,
Dec 15, 2017, 5:51:55 PM12/15/17
to ISO C++ Standard - Future Proposals
Declaring generator<int> with the nodiscard attribute might help here. Has it been considered yet?

Alberto Barbati

unread,
Dec 16, 2017, 5:42:26 AM12/16/17
to ISO C++ Standard - Future Proposals
Il giorno venerdì 15 dicembre 2017 23:51:55 UTC+1, Alberto Barbati ha scritto:
> Declaring generator<int> with the nodiscard attribute might help here. Has it been considered yet?

Actually I would as far as proposing to add the following note to [dcl.fct.def.coroutine]:

[Note: implementations are encouraged to treat calls to coroutines as nodiscard calls ([dcl.attr.nodiscard]). - end note]

Implementors are allowed and might probably do it anyway, but a note shouldn’t hurt. What do you think?

Arthur O'Dwyer

unread,
Dec 16, 2017, 10:17:25 AM12/16/17
to ISO C++ Standard - Future Proposals
Nicol has found the "bug" in the original code: the bug is "you can't do that."  But does anyone (e.g. Nicol) have any discussion on what "you can't do that" means for programming with coroutines in general?  I mean, it sounds like we're saying "you can't use STL algorithms with coroutines — not even algorithms as simple as for_each."  What if someone wanted to write a non-trivial coroutine? Is that a good idea? Bad idea?

I mean, I personally never got on board the "all STL algorithms all the time" boat to begin with, so my for-loop-heavy code would translate into coroutine-land all right (as long as I debarked from the "all five-line functions all the time" boat).  But if someone was used to writing std::copy and std::for_each and std::generate and so on, they wouldn't be able to translate that style into coroutine-land? They'd have to go back to native for-loops?  Or will Coroutines TS also add std::co_for_each and std::co_generate and so on?
In general, are we bifurcating the library again?

–Arthur

Jim Hurd

unread,
Dec 16, 2017, 11:36:24 AM12/16/17
to std-pr...@isocpp.org
I did check and at least in vc++ [[nodiscard]] offers no further warning. I did:

template <typename T> struct [[nodiscard]] recursive_generator { ... }

recursive_generator<int> f2()
{
    auto g = { 1,2,3 };
    // this does work
    for (auto e : g) co_yield e*2;
    // this compiles fine, and might be expected to work, but doesn't
    // each iteration only initializes the coroutine, but doesn't yield anything
    // using a recursive_generator makes no difference
    std::for_each(g.begin(), g.end(), [&](int x)->recursive_generator<int> {co_yield x * 2; });
    co_return;
}

No warning. for_each dutifully creates each generator, but of course it doesn't yield anything.

--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+unsubscribe@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/534a0ae5-1d4a-4566-838c-59e169ad58fd%40isocpp.org.

Nicol Bolas

unread,
Dec 16, 2017, 11:37:05 AM12/16/17
to ISO C++ Standard - Future Proposals
On Saturday, December 16, 2017 at 10:17:25 AM UTC-5, Arthur O'Dwyer wrote:
Nicol has found the "bug" in the original code: the bug is "you can't do that."  But does anyone (e.g. Nicol) have any discussion on what "you can't do that" means for programming with coroutines in general?  I mean, it sounds like we're saying "you can't use STL algorithms with coroutines — not even algorithms as simple as for_each."  What if someone wanted to write a non-trivial coroutine? Is that a good idea? Bad idea?

I think you're touching on a key issue, but you may not be stating it correctly. Or at least, not fully.

The fundamental issue I have had with the Coroutines TS is that they aren't coroutines. They are continuations. They are nothing more than a way to halt the progress of a function, "schedule" its resumption, and return a value to the caller which allows the caller to track the progress of the continuation.

So if you want to have a call stack halt its progress and schedule the entire stack's continued execution (which is precisely the case when you want to `co_await`/`co_yield` through an algorithm), the only way to do that is to have every function between the first caller and the first one that does the halting to be involved in that process. Every single one of them must `co_await` on some future-like object, and every function involved in that chain has to be a coroutine function.

Which effectively means that passing coroutine functors is... dubious. It's not always wrong, but it should be looked at with suspicion.

This is why I much prefer P0099/P0534 coroutines. You can halt them anywhere within the coroutine stack and relay values back to the destination. The principle downside (besides syntax) of this is the huge cost incurred by having to allocate a call stack for the coroutine. That makes using them for light-weight generators problematic.

The principle advantage of the Coroutines TS is that their continuation syntax makes complex asynchonrous code look almost exactly like the synchronous equivalent. To the point where you barely notice that it's async code.

Here's some slow synchronous networking code:

auto data = network::get_data(...);
process_data
(data);
network
::send_data(feedback(data), ...);

And here's the async version of it:

auto data = co_await network::async_get_data(...);
process_data
(data);
co_await network
::async_send_data(feedback(data), ...);

I love the library coroutine proposals, but they can never be that easy to read.

Unfortunately, so long as the primary proponents of the Coroutines TS remain singularly uninterested in re-evaluating their design and doing the some experimental work to retain the beauty of the syntax while allowing multiple stack frames, these flaws will remain. While lots of papers were written about alternative language mechanisms for real coroutines, none of those authors have the resources to commit to actually implement any of them in a compiler.

So however flawed the Coroutine TS certainly is, it is at least backed by existing practice, rather than being something entirely speculative. If we could get people with the time, resources, and domain knowledge to get a better language feature, I suspect the committee might consider it, either as an alternative or as a replacement.

Indeed, if I recall correctly, that was part of the point of shoving P0057 in a TS rather than the standard itself.

I mean, I personally never got on board the "all STL algorithms all the time" boat to begin with, so my for-loop-heavy code would translate into coroutine-land all right (as long as I debarked from the "all five-line functions all the time" boat).  But if someone was used to writing std::copy and std::for_each and std::generate and so on, they wouldn't be able to translate that style into coroutine-land?

Not if they want to execute a `co_yield` through the algorithm.

They'd have to go back to native for-loops?  Or will Coroutines TS also add std::co_for_each and std::co_generate and so on?
In general, are we bifurcating the library again?

There have been no proposals towards that end. Algorithm combinations with coroutines tend to be for generators rather than "up-and-out" continuations. As such, how useful such algorithms would be depends entirely on how much people want generators, as opposed to the current paradigm of ranges/iterators/etc.

Alberto Barbati

unread,
Dec 18, 2017, 6:10:35 AM12/18/17
to ISO C++ Standard - Future Proposals
Il giorno sabato 16 dicembre 2017 17:36:24 UTC+1, Jim Hurd ha scritto:
I did check and at least in vc++ [[nodiscard]] offers no further warning. I did:

[snip]
No warning. for_each dutifully creates each generator, but of course it doesn't yield anything.


Visual Studio might simply ignore [[nodiscard]] (which would be annoying, but perfectly conforming) or it might just be a bug. I don't have an implementation of Visual Studio at hand right now, does [[nodiscard]] work in other contexts?

Alberto Barbati

unread,
Dec 18, 2017, 6:41:18 AM12/18/17
to ISO C++ Standard - Future Proposals
Il giorno sabato 16 dicembre 2017 16:17:25 UTC+1, Arthur O'Dwyer ha scritto:
Nicol has found the "bug" in the original code: the bug is "you can't do that."  But does anyone (e.g. Nicol) have any discussion on what "you can't do that" means for programming with coroutines in general?  I mean, it sounds like we're saying "you can't use STL algorithms with coroutines — not even algorithms as simple as for_each."  What if someone wanted to write a non-trivial coroutine? Is that a good idea? Bad idea?

Please notice that the "bug" Nicol found has nothing to do with std algorithms, but rather with an unrealistic expectation of the OP about the interaction between coroutines and lambda expressions. In other words, the same "bug" occurs in this code:

generator<int> f2()
{
    auto lambda = [](int x) ->generator<int> {co_yield x * 2; };
    lambda(); // does NOT execute co_yield
    co_return;
}

Let's change the code a bit and replace the lambda with a real function:

generator<int> lambda(int x)
{
    co_yield x * 2;
}

generator<int> f2()
{
    lambda(); // does NOT execute co_yield
    co_return;
}

Here you see that the co_yield in function lambda() has nothing to do with the coroutine context of f2(): it appertains to a different coroutine context. Under the TS-proposed coroutine model, the co_yield in lambda() does not automatically "pass through" in order to let f2() produce a value. f2() must willingly opt-in for this to happen, by first calling co_await (to actually compute and get the value yielded by lambda) and then co_yield (to eventually make the value available to the f2() caller). Is this cumbersome? Maybe. As Nicol remarked, there are other coroutine models (some of them actually being proposed for C++), however the model we currently have is designed like this and it's probably here to stay, whether you like or not.

As for the issue "coroutines vs. algorithms", the library support for coroutines is still in its infancy. We might probably need to have new set of coroutine-friendly algorithms. As we have now a co_await-for statement, in addition to the "classic" for statement, we might think about a std::co_for_each algorithm, for example, that co_awaits each invocation of its argument. We just need some more usage experience to find the good algorithms, I guess.
Reply all
Reply to author
Forward
0 new messages