C++ coroutines do not spark joy (2021)
probablydance.com
probablydance.com
You can also specify custom new/delete operations for your co-routine for control. I am not sure if you are allowed to delete them to guarantee elision either happens or the program fails to compile.
Much like lambdas and ranged-based for loops, co-routines are pretty much defined as a code transformation into lower-level C++ rather than being black magic.
Regarding the "inline" keyword being used to move a function's body into the caller: that is a compiler hint and not mandatory. The actual purpose of the "inline" function is to allow the implementation of a function to appear in multiple translation units without that causing linking issues (provided the implementation is identical).
In terms of "I’m curious if anyone actually finds something useful to do with these.": the modern C++ Windows Runtime API is built upon async operations and co-routines.
And this answers another question from the article:
> in C++ I don’t know who [coroutines] are for, or who asked for this…
And the answer is Microsoft. They already had an extension for coroutines in their compiler before the standard, the standard is not very different from their original implementation (albeit there are differences), and you can almost always see at least one @ms address in all the discussions.
This is quite common. Especially for complex proposals, the committee wants field experience or at least proof that it is implementable before standardization.
Unity (and probably others) use yielding iterators as a somewhat hacky way to make coroutines, though. I wouldn't say its a good role model but it gets the job done.
> Give me access to the generated struct. Allow me to put it on the stack of another function. Or as a member of another struct. Then I can store it on the heap if I want, but don’t force me.
As for why they're allocated on the heap...seems like the stack would be a foot gun, no? To write a safe coroutine, you need to hope your caller doesn't blow out the stack between iterations or just defensively put everything on the heap yourself anyway. Enforcing safety seems like the right call but that's a matter of taste, I suppose.
This does remind me of a recent C# change to the Task<T> promise type returned by async/await functions. Before they were only heap allocated because you need that to be able to properly await them multiple times. This is a major reason you can't use them in games and use things like Unity's Coroutines. As it turns out a lot of small heap allocs suck and stack allocation does come in handy for perf. Now we have stack allocated Task<T> types as well. It would be a nightmare if the unsafe was the default though... Why is life so complicated?
I think the conflict is between "yielding another value" and "yielding control back to the scheduler". Both of them use "yield" but they're different types of yield, and people use the same keyword for both, and either one can be implemented in terms of the other, and so on.
Still, I don't think iterators and coroutines are necessarily the same. You can imagine a coroutine that doesn't return a value and only happens to share a thread or something.
> Even weirder to say its a "C# coroutine" feature when there's no coroutine in the C# language.
Unity has a coroutine system based on iterators, maybe that's where the confusion is coming from:
https://docs.unity3d.com/Manual/Coroutines.html
...or simply because async/await, coroutines or any other multitasking system serves the same purpose for the user: to kick off background tasks.
> Enforcing safety seems like the right call
It certainly is, however it is hard to argue this point while looking at other more or less recent features:
string_view is basically a fancy "char *", with all the same use-after-free issues (same with other pointer range types and iterators)
Lambas with capture-by-reference can be freely copied and type erased into function<>, so obviously they did not care about safety when that feature got included
That said, span (unlike string_view) fails at its intended purpose, because C++ just loves to be a total clown show.
https://learn.microsoft.com/en-us/archive/msdn-magazine/2019...
Because of the performance? Or for another reason?
The days of avoiding malloc in games is long since passed. Of course, fewer allocations, like reducing any work, is always better.
The concept of coroutines (Melvin E. Conway in 1963; who was also the source of FORK/JOIN) predates by a decade that of iterators/generators (which come from Alphard and CLU).
The term "yield" had already been used for many years for coroutines before being used for iterators/generators.
Usually the use of "yield" is not ambiguous, because the context is clear.
A C# async method is also a coroutine.
C++ Coroutines Do Not Spark Joy - https://news.ycombinator.com/item?id=29064233 - Nov 2021 (250 comments)
C++23 introduces std::generator which is roughly similar to Python and JavaScript generators, implemented on top of coroutines. If that model fits your application, you don’t need to mess with the coro API directly.
Edit: @gloryjulio I'm sure it works fine, it's just so unergonomic to express compared to Golang. But this is nothing new with C++, it can do anything.. might not be pretty, but it can do it.
Calling coroutine in non-coroutine code is also easy, just do blockingWait(co_routineFunc());
We almost never use multithreading code in the business layer now unless coroutine is actually causing performance issue. I haven't seen that happen yet
And its pretty easy to use the same workflow as goroutines in C++ using std::thread and a concurrent queue implementation, tbb has a decent one and moodycamels is also very good.
It's actual async/await that is a touch annoying but it's also fundamental building blocks, there are currently few abstractions ontop of it.
I also believe strongly async/await is an antipattern. There are better models to handle concurrency well imho that don't use function colouring. Golangs goroutines and channels (effectively actors but that's a can of worms) are a great solution and probably what you should be reaching for instead of async/await.
Goroutines are parallel, coroutine are concurrent. Those are not the same thing.
Coroutines co-operatively yeild to other coroutines. Parallel constructs run in parallel at the same time. Goroutines can run in parallel at the same time and thus are not a coroutine.
You can run a C++ coroutine on a seperate thread but you effectively need to build all the task handling logic yourself of use a library that's build that out. Goroutines you don't need to use a specialised sleep fuction which is actually changing coroutine state, setting a timer and yeilding. You just sleep as an example
The C++ approach is fully compatible with the C ABI, since in the end it's all just callbacks once you strip all the abstraction layers. Which also means that other languages that use this approach (among them C#, Python, and JavaScript) can easily perform async calls across the interop boundary; this is actually used on Windows, with the OS projecting a common set of async APIs to various languages.
With Go, if you want to do async with some code that's not yours, it has to also be written in Go.
[1] https://www.scs.stanford.edu/~dm/blog/c++-coroutines.html
Meanwhile, the same optimizations one could use for efficient coroutines could just as well be applied to fibers, which are much nicer.
You end up requiring more sophisticated object pools where you can check out and check in objects.
It is very hard to workaround that in your code. The only practical solution is to never migrate coroutines to other threads.
Search for talks from Gor Nishanov.