Skip to content

Instantly share code, notes, and snippets.

@dpiponi
Last active March 15, 2021 00:31
Show Gist options
  • Save dpiponi/384c1e97ffad5ed06f3cebddb72e9721 to your computer and use it in GitHub Desktop.
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.)
// 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