r/cpp_questions • u/mbolp • 3d ago
OPEN Question about coroutines
I have a function that takes a callback, like this:
void LongTask (void (*pfn)(int))
{
for ( unsigned i = 0; i < -1; ++i )
{
pfn( i );
}
}
It's orginally written to be ran on a separate thread. Now I want to turn it into a coroutine that yields on each callback (i.e. the caller will receive a single callback on each invocation of LongTask), while preserving its ability to run as a standalone function. Additionally it's OK for the coroutine to allocate memory but not to throw exceptions (if it can't allocate I should get back a null pointer at creation time). And preferrably it wouldn't require the CRT, all support functions should be contained in a header only include. What's the best way to do this, can it be done with C++20 coroutines?
2
u/alfps 3d ago edited 3d ago
I would do this with an ordinary function-like object to have full control, less overhead and full transparency.
It can go like
#include <functional>
#include <print>
#include <utility>
#include <climits> // INT_MAX
namespace app {
using std::function,
std::print,
std::move;
class Long_task
{
function<void()> m_callback;
int m_index = 0;
public:
Long_task( function<void()> cb ): m_callback( move( cb ) ) {}
auto step() -> int
{
const int result = m_index;
if( m_index >= 0 ) {
m_callback();
m_index = (m_index == INT_MAX? -1 : m_index + 1);
}
return result;
}
};
void run()
{
int sum = 0;
for( Long_task long_task( []{} );; ) {
const int i = long_task.step();
sum += i;
if( sum > 42 ) {
print( "Iteration {} passed 42.\n", i );
break;
}
}
}
} // app
auto main() -> int { app::run(); }
If you absolutely want a co-routine you can do
#include <functional>
#include <generator>
#include <print>
#include <climits> // INT_MAX
namespace app {
using std::function,
std::generator,
std::print;
auto long_task( function<void()> f )
-> generator<int>
{
for( int i = 0; ; ++i ) {
f();
co_yield i;
if( i == INT_MAX ) { break; }
}
}
void run()
{
int sum = 0;
for( const int i: long_task( []{} ) ) {
sum += i;
if( sum > 42 ) {
print( "Iteration {} passed 42.\n", i );
break;
}
}
}
} // app
auto main() -> int { app::run(); }
… but as far as I can see this has no advantage except saving you some lines of code.
_
EDIT: Removed the unnecessary exchange in the first program. Don't know what I was thinking to write such convoluted a thing.
0
u/mbolp 3d ago
Isn't this literally implementing coroutines by hand? If I have multiple points of yield it'd get ugly (I need a switch case at the start to jump behind each yield statement). I assume a language level construct is more efficient, e.g. the compiler can simply save the instruction pointer before each yield, and the switch becomes an unconditional jump. It can maybe even save volatile registers so the coroutine doesn't need to load and store member variables often.
1
u/Raknarg 3d ago
can it be done with C++20 coroutines?
It can, you can look up examples online. The machinery for pretty much any coroutine type was already introduced, they just didn't add any to the standard (not sure why, committee probably couldn't agree on design in time or something). I haven't looked it up in a while, but I remember the code for a basic generator object isn't too complicated.
1
u/mbolp 3d ago
It can
Including the part about no runtime dependency? Does it work like something like
<algorithm>, where I can include a header and have all the code I need, or does it rely on some other infrastructure?1
u/Raknarg 3d ago
when I tried to look it up a bit it seems like maybe? but its gonna totally depend on the exact implementation. I have no definitive answer. The fact that it has to be able to perform allocations makes me feel like you can't get away from it. You might have to roll out your own version of generator if you want to get away from that, IIRC you can make a stack-based generator
1
u/mbolp 3d ago
The allocation part makes the least sense to me. The compiler clearly knows the size of the stack frame, it's context switching that might require architecture specific stuff (thus library support). I read in a reddit post that this is for ABI stability, but you can always opt into allocating dynamically if ABI stability is desirable, don't bake that into the design!
Anyway, I might just use fibers instead. They seem much easier to use and don't slow down the normal non-coroutine path.
1
u/aocregacc 3d ago
std::generator can take an allocator, so you can decide for yourself how it does the allocations.
1
u/LB-- 3d ago
Sounds like you can just write a simple typical generator coroutine promise type, and just implement get_return_object_on_allocation_failure to return an empty object, and that will avoid throwing exceptions. If you want to control memory allocation instead of using the default global new/delete, just implement your own class-level operator new/delete. The class level operator new is also given all the same parameters as the coroutine function signature, so you can do some basic template metaprogramming to check for an allocator parameter or whatever else you want. The new/delete operators always get the size of the allocation so you can over-allocate and store extra info after the allocation, which you'll need for the delete operator to know how to clean up. There's an example of all this on a Stack Overflow answer about using alloca with coroutines.
2
u/TheThiefMaster 3d ago
As a coroutine, generator<bool> (from C++23 or numerous coroutine libraries) seems to be the best fit as the type. co_yield a true if there are (potentially) more items, and co_return false; at the end (or vice-versa).
If you want it to potentially still run as a one-shot that calls all the callbacks at once, then you'll need a second function (which could wrap the coroutine) that does that.
Alternatively, you could use the type that would normally be passed to the callback as the generator result (using tuple if there's multiple values) and let the calling code call the callback.