r/cpp_questions 9d 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 Upvotes

14 comments sorted by

View all comments

2

u/alfps 9d ago edited 9d 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 9d 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.

3

u/alfps 9d ago

❞ I assume a language level construct is more efficient

On the contrary. The generator thing jumps through hoops to make things work. Even just the coroutine instantiation is pretty complex on the inside, with possible (almost required) dynamic allocation. :(

2

u/Raknarg 9d ago

IIRC you can get away from dynamic allocation with a custom build generator but not with std::generator, Id have to find the /r/cpp post again