4 ms·
I'll take the bait. Here's a coroutine waitFrames(5); // wait 5 frames fireProjectile(); waitFrames(15); turnLeft(-30/*deg*/, 120); // turn lef
by socalgal2 7mo ago
I'll take the bait. Here's a coroutine
waitFrames(5); // wait 5 frames
fireProjectile();
waitFrames(15);
turnLeft(-30/*deg*/, 120); // turn left over 120 frames
waitFrames(10);
fireProjectile();
// spin and shoot
for (i of range(0, 360, 60)) {
turnRight(60, 90); // turn 60 degrees over 90 frames
fireProjectile();
}
10 lines and I get behavior over time. What would your non-coroutine solution look like?
- mgaunard 7mo agoGiven a coroutine body ``` int f() { a; co_yield r; b; co_return r2; } ``` this transforms into ``` auto f(auto then) { a; return then(r, [&]() { b; return then(r2); }); }; ``` You can easily extend this to arbitrarily complex statements. The main thing is that obviously, you have to worry about the capture lifetime yourself (coroutines allocate a frame separate from the stack), and the syntax causes nesting for every statement (but you can avoid that using operator overloading, like C++26/29 does for executors)
- spacechild1 7mo agoHow is this better than the equivalent coroutine code? I don't see any upsides from a user's perspective. > The main thing is that obviously, you have to worry about the capture lifetime yourself This is a big deal! The fact that the coroutine frame is kept alive and your state can just stay in local variables is one of the main selling points. I experienced this first-hand when I rewrote callback-style C++ ASIO code to the new coroutine style. No more [self=shared_from_this()] and other shenanigans!
- mgaunard 7mo agoUsing shared_ptr everywhere is an antipattern. The whole point of controlling the capture is controlling the memory layout, which is what C++ is all about. Even with Asio, you don't really have to do this. It's just the style the examples follow, and Asio itself isn't necessarily the best design.
- spacechild1 7mo agoWith callbacks you have to make sure that your data persists across the function calls. This necessarily requires more heap allocations (or copies) than in a coroutine where most data can just live on the stack.
- mgaunard 7mo agoA coroutine doesn't do anything more than a callback does -- it's just syntactic sugar. The default behaviour of many asynchronous systems is to extend the lifetime of context data until all the asynchronous handlers have run. You can also just bind them to the resource instead which is arguably more elegant, but which depends on how cancellation is implemented.
- spacechild1 7mo agoAny data that needs to survive across several callback invocations requires shared ownership. This means that the data must be allocated on the heap and probably also requires reference counting. With lambdas it's also easy to make mistakes, e.g. by capturing variables on the stack by reference. In a coroutine everything can just live on the stack because the stack frame itself is kept alive across the asynchronous function calls. Don't you see the big difference regarding lifetime management?
- mgaunard 7mo agoNot necessarily, I already explained how (resource binding). Close the socket, all handlers are cancelled, context can be freed immediately. The socket owns the data.
- spacechild1 7mo ago> The socket owns the data. What if there's more than one socket involved?
- krackers 7mo agoIsn't this basically what javascript went through with Promise chaining "callback hell" that was cleaned up with async/await (and esbuild can still desugar the latter down to the former)
- mgaunard 7mo agoThis is literally what coroutines are, syntactic sugar to generate nested lambdas. Except in C++ this removes a fair amount of control given how low-level it is.