Last active
November 4, 2024 06:09
-
-
Save dpiponi/384c1e97ffad5ed06f3cebddb72e9721 to your computer and use it in GitHub Desktop.
Near minimal example of replacing explicit state machine with coroutine. (Compare Example1 class with Example2, not the supporting "library" code.)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Some platforms import from experimental/coroutine | |
// rather than coroutine. | |
#define REQUIRES_EXPERIMENTAL 1 | |
// Controls whether the coroutine starts in a suspended state | |
// or runs to the first suspension point on construction. | |
// The two implementations should match. | |
#ifndef INITIAL_RUN | |
#define INITIAL_RUN 1 | |
#endif | |
// WIth clang on MacOS I used `c++ -DINITIAL_RUN=<0 or 1> -std=c++2a -o simple simple.cpp`. | |
#include <iostream> | |
#if REQUIRES_EXPERIMENTAL | |
#include <experimental/coroutine> | |
using namespace std::experimental; | |
#else | |
#include <coroutine> | |
#endif | |
using namespace std; | |
// The old school way. | |
class Example1 | |
{ | |
enum State { | |
START, | |
MIDDLE, | |
END | |
} state; | |
public: | |
Example1() : state(START) | |
{ | |
#if INITIAL_RUN | |
step(); | |
#endif | |
} | |
void step() | |
{ | |
switch (state) | |
{ | |
case START: | |
cout << "1. Start" << endl; | |
state = MIDDLE; | |
break; | |
case MIDDLE: | |
cout << "1. Middle" << endl; | |
state = END; | |
break; | |
case END: | |
cout << "1. End" << endl; | |
break; | |
} | |
} | |
void reset() | |
{ | |
state = START; | |
#if INITIAL_RUN | |
step(); | |
#endif | |
} | |
}; | |
// The coroutine way. | |
// This is the state machine coroutine class. | |
// You only need to implement this once. | |
struct Coroutine | |
{ | |
struct promise_type; | |
coroutine_handle<promise_type> handle; | |
Coroutine(const coroutine_handle<promise_type>& h) : handle(h) | |
{ | |
} | |
Coroutine(const Coroutine&) = delete; | |
Coroutine &operator=(Coroutine& Other) = delete; | |
Coroutine(Coroutine&& Other) : handle(Other.handle) | |
{ | |
Other.handle = nullptr; | |
} | |
Coroutine &operator=(Coroutine&& Other) | |
{ | |
if (handle) | |
{ | |
handle.destroy(); | |
} | |
handle = Other.handle; | |
Other.handle = nullptr; | |
return *this; | |
} | |
~Coroutine() | |
{ | |
if (handle) | |
{ | |
handle.destroy(); | |
} | |
} | |
// The name `promise_type` is fixed by the C++ standard. | |
struct promise_type | |
{ | |
~promise_type() | |
{ | |
// It's easy to accidentally fail to call the promise | |
// destructor (or implicitly allow it to get called) | |
// so this line allows us to see it actually | |
// happen. | |
// This should be displayed three times. | |
cout << "~promise_type::promise_type()" << endl; | |
} | |
// Use `suspend_never` if you'd like to run up to the first | |
// `co_await` on construction. | |
#if INITIAL_RUN | |
suspend_never | |
#else | |
suspend_always | |
#endif | |
initial_suspend() | |
{ | |
return {}; | |
} | |
// With `suspend_never` the promise will destroy itself | |
// at the end of the coroutine but then I can lose | |
// track of whether a promise still exists. | |
// This makes things more explicit. | |
suspend_always final_suspend() | |
{ | |
return {}; | |
} | |
void return_void() | |
{ | |
} | |
// When we call the coroutine the runtime creates `this` promise | |
// from which we construct a `coroutine_handle<>` which we use | |
// to construct a `Coroutine` object with is then returned to | |
// the caller as the return value from `run()`. | |
Coroutine get_return_object() | |
{ | |
return Coroutine(coroutine_handle<promise_type>::from_promise(*this)); | |
} | |
void unhandled_exception() | |
{ | |
// Put something here... | |
} | |
}; | |
}; | |
class Example2 | |
{ | |
Coroutine coroutine; | |
Coroutine run() | |
{ | |
cout << "2. Start" << endl; | |
co_await suspend_always(); | |
cout << "2. Middle" << endl; | |
co_await suspend_always(); | |
cout << "2. End" << endl; | |
co_return; | |
} | |
public: | |
Example2() : coroutine(run()) { } | |
void step() | |
{ | |
coroutine.handle.resume(); | |
} | |
void reset() | |
{ | |
coroutine = run(); | |
} | |
}; | |
int main() | |
{ | |
// Old school way. | |
Example1 simple1; | |
// We can omit one `step()` if we don't suspend | |
// at start. | |
#if !INITIAL_RUN | |
simple1.step(); | |
#endif | |
simple1.step(); | |
simple1.reset(); | |
#if !INITIAL_RUN | |
simple1.step(); | |
#endif | |
simple1.step(); | |
simple1.step(); | |
simple1.reset(); | |
#if !INITIAL_RUN | |
simple1.step(); | |
#endif | |
simple1.step(); | |
simple1.step(); | |
cout << "-----" << endl; | |
// Coroutine way. | |
Example2 simple2; | |
#if !INITIAL_RUN | |
simple2.step(); | |
#endif | |
simple2.step(); | |
simple2.reset(); // Check we can reset from incomplete state. | |
#if !INITIAL_RUN | |
simple2.step(); | |
#endif | |
simple2.step(); | |
simple2.step(); | |
simple2.reset(); // Check we can reset from complete state. | |
#if !INITIAL_RUN | |
simple2.step(); | |
#endif | |
simple2.step(); | |
simple2.step(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment