Finite State Machine with std::variant in C++

Table of Contents

Finite State Machines (FSMs) are essential tools in computer science and software engineering for modeling and controlling stateful systems. They provide a structured way to represent and manage the state transitions of a system. In modern C++, the std::variant class from the C++17 standard can be used effectively to implement Finite State Machines. In this article, we’ll explore the concept of FSMs and demonstrate how to implement one using std::variant.

What is a Finite State Machine?

A Finite State Machine is a mathematical abstraction used to represent systems with a finite number of states and transitions between those states. FSMs are widely used in various fields, including software development, control systems, and artificial intelligence.

An FSM typically consists of the following components:

  1. States: These represent the distinct conditions or modes that a system can be in. States are finite, well-defined, and mutually exclusive.
  2. Transitions: Transitions define how the system moves from one state to another based on certain conditions or events.
  3. Events/Inputs: Events or inputs trigger state transitions. These could be user actions, sensor readings, or any other external stimulus.
  4. Actions/Outputs: Actions define what happens when a particular transition occurs. They can involve changing internal variables, generating outputs, or performing other operations.

Implementing FSM with std::variant

To implement an FSM using std::variant, we’ll use the variant to represent the different states of the machine. Each state is a distinct type within the variant. Transitions are implemented by changing the active state within the variant.

Let’s create a simple example of an FSM for a traffic light with three states: Red, Yellow, and Green. We’ll define transitions based on a timer.

#include <iostream>
#include <variant>
#include <chrono>
#include <thread>

// Define the states as types within std::variant
struct RedState {};
struct YellowState {};
struct GreenState {};

// Define the Finite State Machine using std::variant
using TrafficLightFSM = std::variant<RedState, YellowState, GreenState>;

// Function to handle the transition logic
void transition(TrafficLightFSM& state) {
    // Implement transition logic here
    if (std::holds_alternative<RedState>(state)) {
        state = YellowState{};
    } else if (std::holds_alternative<YellowState>(state)) {
        state = GreenState{};
    } else if (std::holds_alternative<GreenState>(state)) {
        state = RedState{};
    }
}

int main() {
    TrafficLightFSM currentState = RedState{};

    while (true) {
        std::this_thread::sleep_for(std::chrono::seconds(2)); // Simulate a timer

        // Transition to the next state
        transition(currentState);

        // Print the current state
        if (std::holds_alternative<RedState>(currentState)) {
            std::cout << "Traffic Light: Red\n";
        } else if (std::holds_alternative<YellowState>(currentState)) {
            std::cout << "Traffic Light: Yellow\n";
        } else if (std::holds_alternative<GreenState>(currentState)) {
            std::cout << "Traffic Light: Green\n";
        }
    }

    return 0;
}

In this example, we define the states (Red, Yellow, Green) as distinct types within std::variant. The transition function handles the state transitions, and the main loop simulates a timer to trigger transitions.

Benefits of Using std::variant for FSMs

Using std::variant for implementing Finite State Machines offers several advantages:

  1. Type Safety: std::variant ensures type safety by allowing only one state type to be active at a time. This helps catch type-related errors at compile-time.
  2. Clarity: The use of distinct types for states makes the code more self-documenting and readable, as it clearly defines the states and transitions.
  3. Maintainability: Adding or modifying states is straightforward, as you only need to define new types and update transition logic.
  4. Efficiency: std::variant is designed to be efficient and has minimal runtime overhead compared to other approaches.

While the previous section covered the fundamentals of implementing Finite State Machines (FSMs) with std::variant, there are more advanced features and techniques you can explore to enhance your FSM implementation. Let’s dive deeper into some of these aspects:

Event-Driven Transitions

In a real-world application, FSMs often rely on external events or inputs to trigger state transitions. To implement event-driven transitions with std::variant, you can modify the transition logic to accept events as parameters:

// Define the events as types within std::variant
struct TimerExpiredEvent {};
struct ButtonPressedEvent {};

// Modify the transition function
void transition(TrafficLightFSM& state, const std::variant<TimerExpiredEvent, ButtonPressedEvent>& event) {
    // Implement event-driven transition logic here
    if (std::holds_alternative<TimerExpiredEvent>(event)) {
        // Transition logic for timer expiration
    } else if (std::holds_alternative<ButtonPressedEvent>(event)) {
        // Transition logic for button press
    }
}

With this approach, you can transition between states based on various events, such as a timer expiration or a button press.

Hierarchical FSMs

In more complex systems, it’s common to have hierarchical FSMs, where each state can have its own internal FSM. You can achieve this by nesting std::variant instances:

using SubStateMachine = std::variant<SubState1, SubState2>;

struct MainState {
    SubStateMachine subState;
};

This structure allows you to create FSMs with multiple layers of hierarchy, enabling you to model intricate state machines.

Guards and Actions

FSMs often involve additional logic to decide whether a transition should occur (guards) or to perform some actions when transitioning between states. You can implement guards and actions as part of your transition logic:

bool canTransitionFromRedToGreen() {
    // Implement guard logic
    return /* condition */;
}

void performActionsOnTransition() {
    // Implement actions
    // e.g., turn on a light, update a variable, etc.
}

void transition(TrafficLightFSM& state) {
    if (std::holds_alternative<RedState>(state) && canTransitionFromRedToGreen()) {
        state = GreenState{};
        performActionsOnTransition();
    } else if (std::holds_alternative<YellowState>(state)) {
        // Transition logic for YellowState
    } else if (std::holds_alternative<GreenState>(state)) {
        // Transition logic for GreenState
    }
}

Guards and actions allow you to add conditional checks and execute specific code during state transitions.

State Entry and Exit Actions

In more advanced FSM implementations, you may need to execute actions when entering or exiting a state. This can be achieved by calling entry and exit functions:

struct RedState {
    void onEntry() {
        // Code to execute when entering RedState
    }

    void onExit() {
        // Code to execute when exiting RedState
    }
};

// Modify the transition function to call entry and exit functions
void transition(TrafficLightFSM& state) {
    if (std::holds_alternative<RedState>(state) && canTransitionFromRedToGreen()) {
        state.onExit();
        state = GreenState{};
        state.onEntry();
    } else if (std::holds_alternative<YellowState>(state)) {
        // Transition logic for YellowState
    } else if (std::holds_alternative<GreenState>(state)) {
        // Transition logic for GreenState
    }
}

This approach allows you to perform specific actions when entering or exiting a state.

Conclusion

Implementing Finite State Machines with std::variant in C++ provides a flexible and type-safe way to model complex systems. By incorporating event-driven transitions, hierarchical FSMs, guards, actions, entry and exit functions, you can create FSMs that accurately represent the behavior of your applications. As you gain proficiency with this approach, you’ll be well-equipped to handle a wide range of stateful scenarios in your C++ projects. Remember that the key to successful FSM implementation is thoughtful design and clear state transition logic.

Command PATH Security in Go

Command PATH Security in Go

In the realm of software development, security is paramount. Whether you’re building a small utility or a large-scale application, ensuring that your code is robust

Read More »
Undefined vs Null in JavaScript

Undefined vs Null in JavaScript

JavaScript, as a dynamically-typed language, provides two distinct primitive values to represent the absence of a meaningful value: undefined and null. Although they might seem

Read More »