Assume that we have a well know callback solution that might look like this: (the code below is pseudocode in C++ syntax because the question is related to C++20 coroutines, necessary boilerplate code is intentionally omitted to make all this more readable)
class ICallback
{
public:
virtual int callbackA(T param1, U param2, V param3)=0;
virtual int callbackB(int a, double b)=0;
virtual int callbackC(T1 param1, T2 param2)=0;
};
And we have some class that is processing some data and sometimes it calls any of the 3 callback methods:
class Processor
{
public:
explicit Processor(ICallback* callback) : m_callback(callback){} // assume 'callback' cannot be nullptr
int process(const uint8_t* data, int size)
{
int bytes_processed = 0;
while(bytes_processed != size)
{
int bytes_consumed_in_this_iteration = doSomething(data + bytes_processed, size - bytes_processed);
// Assume that doSomething() might call any callback method through m_callback!!
bytes_processed += bytes_consumed_in_this_iteration;
}
return size; // return value is irrelevant
}
private:
int doSomething(const uint8_t* data, int size){ /* assume something useful is going on here */ }
ICallback* m_callback;
};
Just assume that this code above is functional, so we receive calls 'callbackA()', 'callbackB()' and 'callbackC()'.
Now comes the funny part. Coroutines are at this moment a miracle given by gods. They can do everything and more, much more. Allegedly they can replace "callback hell" because they are perfect. So, I tried to do it (and failed). To be honest, coroutines are much bigger hell than all callbacks in the world.
I do understand the entire mambo-jumbo boilerplate code that is required behind things like T::promise_type and promise_type's methods like get_return_object(), return_value() or return_void(), initial_suspend(), final_suspend(), etc... so I won't contaminate with that boilerplate code here. The same applies for our custom awaiter object. Therefore, lets jump to the coroutine itself.
SomeType startCoroutine(/* don't really know what to put here*/)
{
// First, I don't know how to feed a coroutine with data. What if I receive data over a socket?
// I don't expect anyone to write the actual code, a comment and pseudocode with the most crucial parts should do
// even if we ignore that hard part of delivering input data to the coroutine and simply assume that it's here even though we don't know how it arrived, what's next?
byte data[100]; // assume it contains valid payload
co_await std::make_pair(data, sizeof(data)); // calls promise_type::await_transform(std::pair<const uint8_t*, const uint32_t> value), it creates an instance of 'awaiter' and calls awaiter::await_ready() and then awaiter::await_resume() or awaiter::await_suspend(), depending on the awaiter::await_ready's() return value.
// so, if we implement in our awaiter::await_resume() or in awaiter::await_suspend() something like Processor::process() from the beginning, but without callbacks, what should be done if after processing first 2 bytes we detect that now is the time to return control to the caller in the coroutine because we don't use callbacks any more? It can be done via await_resume(), but that returns the result, so we cannot go back and process the remaining 98 bytes of our data. If we remain in the suspended state, what then? We can resume, but resuming will call await_resume() again.
}
First define some types holding the same data passed to the callbacks:
Now we change
Processor::processinto a free-function coroutine, perhaps with helper functions.Now this does require a type that's only in C++23,
std::generator, but that is a widely applicable library type, you aren't fiddling around with defining a specific promise type yourselfI'm not sure this matches the call pattern that your
doSomethinghas for the callbacks inICallback, I'm assuming at most one callback per invocation ofdoSomething. If there are more, you'd need to return a range ofEvents, and then it'd beco_yield std::ranges::elements_of(events).