04 Feb 2017
C++17 brings us
std::variant that will enable new kinds of type-safe programming patterns by providing the ability to construct sum types. One interesting potential use of
std::variant was presented by Ben Deane in his 2016 CppCon talk “Using Types Effectively”. If you haven’t yet, you should go and watch the talk. It presents many clever ideas on how to use the C++ type system to your advantage.
The idea presented in the talk was to use
std::variant to represent state machines. The main motivation was to make illegal states unrepresentable by embedding the variables only needed in one state inside a structure representing the state. All the state structures are then combined in a
std::variant object to represent the current state. The example used in the talk showed the structure declarations for a state machine modeling some kind of server connection:
The idea is that
m_lastPingTime is only needed in
m_disconnectedTime only in
ConnectionInterrupted state, and so on.
The talk didn’t go further into details on how to implement the full state machine logic with state transitions, transition actions etc. using this pattern. In this post, I explore a possible implementation.
A state transition is made simply by assigning a new value to the state variant,
m_connection in the case of our example. For example, the
Connection class could have a function
that would be called when a disconnection event is detected. This is the simplest transition implementation, as we always move to the same state as a result of the event,
Disconnected in this case, regardless of the state the connection is when the event occurs.
A more interesting situation arises when the transition invoked by the event depends on the current state. This requires us to inspect which alternative the variant currently holds. A natural and safe way to do this is to use
std::visit. The visitor passed to
std::visit is required to be callable with all the alternative types the variant can hold, otherwise the program is ill-formed. This property gives us a compile-time check that we handle all the source states in our transition visitors.
The visitor can also return a value. A useful value to return from a transition visitor is the target state of the transition. As an example, consider a visitor handling a connection interrupted event:
If we get the interruption event when the connection hasn’t been established yet (
Connecting state), we move to (or stay in) the
Disconnected state. From
Connected state we move to
ConnectionInterrupted state. If we are already in the
ConnectionInterrupted state, we stay there.
The visitor can be used like this:
What if we’d like to execute some action in the
Connection class when a transition is initiated in the visitor, for example, notify observers of the
Connection about connection interruption? The visitor needs access to the
Connection object. We can “capture” the object to the visitor:
and pass the reference when calling
See the full code here.