Table of Contents

In this blog post, I’ll show you how to convert a “regular” enum-style finite state machine into a modern version based on std::variant from C++17. This technique allows you to improve design, work with value types and enhance code quality.

States  

Let’s start with a basic example:

  • we want to track a game player’s health status
  • we’d like to respond to events like “Hit by a monster” or “Healing bonus.”
  • when the health point goes to 0, then we have to restart the game only if there are some remaining lives available

Here’s a basic diagram with states and transitions:

Enum-based FSM  

We can quickly write some basic code based on enum classes:

enum class HealthState { PlayerAlive, PlayerDead, GameOver };
enum class Event { HitByMonster, Heal, Restart };

And the state machine class:

class GameStateMachine {
    public:
        void startGame(unsigned int health, unsigned int lives) {
            state_ = HealthState::PlayerAlive;
            currentHealth_ = health;
            remainingLives_ = lives;
        }

        void processEvent(Event evt, unsigned int param) {
            switch (evt)
            {
            case Event::HitByMonster:
                state_ = onHitByMonster(param);
                break;
            case Event::Heal:
                state_ = onHeal(param);
                break;
            case Event::Restart:
                state_ = onRestart(param);
                break;
            default:
                throw std::logic_error{ "Unsupported state transition" };
                break;
            }
        }

    private:
        HealthState state_;
        unsigned int currentHealth_{ 0 };
        unsigned int remainingLives_{ 0 };
    };

The idea is simple:

  • keep the state in state_, but also have a track of currentHealth_ and remainingLives_
  • everything important happens in processEvent, which takes an additional parameter (a generic one)

Here’s the implementation of handling monsters:

HealthState onHitByMonster(unsigned int param) { // param is the force
    if (state_ == HealthState::PlayerAlive) {
        std::cout << std::format("PlayerAlive -> HitByMonster force {}\n", param);
        if (currentHealth_ > param) {
            currentHealth_ -= param;
            return state_;
        }

        if (remainingLives_ > 0) {
            --remainingLives_;
            return HealthState::PlayerDead;
        }

        return HealthState::GameOver;
    }

    throw std::logic_error{ "Unsupported state transition" };
}

That’s probably the largest function. As you can notice, the param argument is now treated as the “force” from the monster. The function checks if the player is still alive after the hit. Depending on the current health, the code returns the same state - Alive, or PlayerDead. It also has to be prepared for the situation with Game over - there’s no health and no remaining lives.

And below, we have two more functions:

HealthState onHeal(unsigned int param) {
    if (state_ == HealthState::PlayerAlive) {
        std::cout << std::format("PlayerAlive -> Heal points {}\n", param);

        currentHealth_+= param;
        return state_;
    }
    throw std::logic_error{ "Unsupported state transition" };
}
HealthState onRestart(unsigned int param) {
    if (state_ == HealthState::PlayerDead) {
        std::cout << std::format("PlayerDead -> restart\n");
        currentHealth_ = param;
        return HealthState::PlayerAlive;
    }
    throw std::logic_error{ "Unsupported state transition" };
}

Example code:  

GameStateMachine game;
game.startGame(100, 1);

try {
    game.processEvent(Event::HitByMonster, 30);
    game.reportCurrentState();
    game.processEvent(Event::HitByMonster, 30);
    game.reportCurrentState();
    game.processEvent(Event::HitByMonster, 30);
    game.reportCurrentState();
    game.processEvent(Event::HitByMonster, 30);
    game.reportCurrentState();
    game.processEvent(Event::Restart, 100);
    game.reportCurrentState();
    game.processEvent(Event::HitByMonster, 60);
    game.reportCurrentState();
    game.processEvent(Event::HitByMonster, 50);
    game.reportCurrentState();
    game.processEvent(Event::Restart, 100);
    game.reportCurrentState();
}
catch (std::exception& ex) {
    std::cout << "Exception! " << ex.what() << '\n';
}

Play with code @Compiler Explorer

Output:

PlayerAlive -> HitByMonster force 30
PlayerAlive 70 remaining lives 1
PlayerAlive -> HitByMonster force 30
PlayerAlive 40 remaining lives 1
PlayerAlive -> HitByMonster force 30
PlayerAlive 10 remaining lives 1
PlayerAlive -> HitByMonster force 30
PlayerDead, remaining lives 0
PlayerDead -> restart
PlayerAlive 100 remaining lives 0
PlayerAlive -> HitByMonster force 60
PlayerAlive 40 remaining lives 0
PlayerAlive -> HitByMonster force 50
GameOver
Exception! Unsupported state transition

Ok, so here we set the initial life to 100 and 1 additional life. The state machine works fine, and as you can see, it correctly recreates the game after the first “death.” Then we got an exception in the GameOver state. In that case, the idea is to restart the whole state machine (it could be implemented differently if we want to).

Pros and cons of the approach  

As you can see, the code works, and it looks simple. Simplicity is the most vital point for this implementation. It’s also quite versatile, and you could write similar code in other programming languages that support enums and switch instructions.

But…

We can mention several weak points:

  • it might get complicated when you have more states and interactions
  • we need to “reinterpret” the generic “param,” so it’s not scalable and readable
  • there’s some code duplication, for example, to detect the unsupported state transition
  • the state is very simple and doesn’t contain any extra “state”/value, so we have to keep additional data as members of the state machine
  • similarly, an event is also simple and cannot pass more data to the event handlers.

Introducing std::variant  

How about throwing std::variant and implementing a state machine?

The main idea is that std::variant is a vocabulary type and supports value semantics. So it might also be relatively simple to use, and there’s no need to implement FSM using pointers, virtual methods, and others.

Let’s try this approach:

This article started as a preview for Patrons, sometimes even months before the publication. If you want to get extra content, previews, free ebooks and access to our Discord server, join the C++ Stories Premium membership or see more information.

Variant-Based State Machine  

Now, rather than relying on enums, we can throw more data into states and events.

namespace state {
    struct PlayerAlive { 
        unsigned int health_{ 0 }; 
        unsigned int remainingLives_{ 0 }; 
    };
    struct PlayerDead { 
        unsigned int remainingLives_{ 0 }; 
    };
    struct GameOver { };
}

using HealthState = std::variant<state::PlayerAlive, state::PlayerDead, state::GameOver>;

namespace event {
    struct HitByMonster { unsigned int forcePoints_{ 0 }; };
    struct Heal { unsigned int points_{ 0 }; };
    struct Restart { unsigned int startHealth_{ 0 }; };
}

using PossibleEvent = variant<event::HitByMonster, event::Heal, event::Restart>;

Now, to handle events, we can implement several functions:

HealthState onEvent(const state::PlayerAlive& alive, 
                    const event::HitByMonster& monster) {
    cout << format("PlayerAlive -> HitByMonster force {}\n", monster.forcePoints_);
    if (alive.health_ > monster.forcePoints_) 
    {
        return state::PlayerAlive{
                 alive.health_ - monster.forcePoints_, alive.remainingLives_
                 };
    }

    if (alive.remainingLives_ > 0)
        return state::PlayerDead{ alive.remainingLives_ - 1 };

    return state::GameOver{};
}

Healing:

HealthState onEvent(state::PlayerAlive alive, const event::Heal& healingBonus) {
    std::cout << std::format("PlayerAlive -> Heal points {}\n", healingBonus.points_);

        alive.health_ += healingBonus.points_;
        return alive;
    }

Restart:

HealthState onEvent(const state::PlayerDead& dead, const event::Restart& restart) {
    std::cout << std::format("PlayerDead -> restart\n");

    return state::PlayerAlive{ restart.startHealth_, dead.remainingLives_ };
}

Game over:

HealthState onEvent(const state::GameOver& over, const event::Restart& restart) {
    std::cout << std::format("GameOver -> restart\n");

    std::cout << "Game Over, please restart the whole game!\n";

    return over;
}

And finally, we can implement a fallback function for unknown state transitions:

HealthState onEvent(const auto&, const auto&) {
    throw std::logic_error{ "Unsupported state transition" };
}

We have all events implemented, and as you can see, the code is more readable. Since the states and events contain additional data, we can now work with properly named parameters rather than rely on “generic” param variables.

The states and events are now self-contained and don’t rely on additional data stored in the state machine.

The state machine class  

Let’s try to connect those pieces together:

class GameStateMachine {
    public:
        void startGame(unsigned int health, unsigned int lives) {
            state_ = state::PlayerAlive{ health, lives };
        }

        void processEvent(const PossibleEvent& event) {
            state_ = std::visit(detail::overload{
               [](const auto& state, const auto& evt) {
                   return onEvent(state, evt);
               }
            },
            state_, event);
        }

    private:
        HealthState state_;
    };

Wow, it’s super simple now!

Since we store events in a separate variant, we can use std::visit with multiple variants. Later we need an overload object to implement a generic event handler.

We could also implement the transitions inside overload, rather than in onEvent functions:

state_ = std::visit(detail::overload{
 [](const state::PlayerAlive& alive, const event::HitByMonster& monster) {
   /* on monster */  
 },
 [](state::PlayerAlive alive, const event::Heal& healingBonus) {
   /* on heal */  
 },
 [](const state::PlayerDead& dead, const event::Restart& restart) {
   /* on restart */  
 },
 [](const state::GameOver& over, const event::Restart& restart) {
   /* on restart in game over... */  
 },
 [](const auto& state, const auto& evt) {
    /* unsupported */
  }
},
state_, event);

The code also works but might get complicated when transitions contain several lines of code each.

Here’s the demo:

GameStateMachine game;
game.startGame(100, 1);

try {
    game.processEvent(event::HitByMonster {30});
    game.reportCurrentState();
    game.processEvent(event::HitByMonster {30});
    game.reportCurrentState();
    game.processEvent(event::HitByMonster {30});
    game.reportCurrentState();
    game.processEvent(event::HitByMonster {30});
    game.reportCurrentState();
    game.processEvent(event::Restart {100});
    game.reportCurrentState();
    game.processEvent(event::HitByMonster {60});
    game.reportCurrentState();
    game.processEvent(event::HitByMonster {50});
    game.reportCurrentState();
    game.processEvent(event::Restart {100});
    game.reportCurrentState();

}
catch (std::exception& ex) {
    std::cout << "Exception! " << ex.what() << '\n';
}

Here’s the demo @Compiler Explorer

and the output:

PlayerAlive -> HitByMonster force 30
PlayerAlive 70 remaining lives 1
PlayerAlive -> HitByMonster force 30
PlayerAlive 40 remaining lives 1
PlayerAlive -> HitByMonster force 30
PlayerAlive 10 remaining lives 1
PlayerAlive -> HitByMonster force 30
PlayerDead, remaining lives 0
PlayerDead -> restart
PlayerAlive 100 remaining lives 0
PlayerAlive -> HitByMonster force 60
PlayerAlive 40 remaining lives 0
PlayerAlive -> HitByMonster force 50
GameOver
GameOver -> restart
Game Over, please restart the whole game!
GameOver

Extensions  

The implementation used PossibleEvent as an additional variant type. But we can also implement it differently:

template <typename Event>
void processEvent(const Event& event) {
    state_ = std::visit(detail::overload{
        [&](const auto& state) {
              return onEvent(state, event);
        }
    },
    state_);
}

This time we can take any event, or even not related type. Then it’s passed as a regular function parameter.

You can read more about this technique in: How To Use std::visit With Multiple Variants and Parameters - C++ Stories

Any disadvantages?  

The solution with variant looks excellent, in my opinion.

But regarding disadvantages, I must mention that each variant is the size of the maximum type it stores (plus a discriminator).

On MSVC, I get:

sizeof(HealthState):   12
sizeof(PossibleEvent): 8

It’s not much, but you might consider what state should be kept in those events and states and how to pass them efficiently. This is not important for simple state machines, but for critical systems, you might see some overhead.

Have a look at this possible solution in the other article: Space Game: A std::variant-Based State Machine by Example - C++ Stories

Summary  

In this article, I showed you a cool technique with std::variant and finite state machines. Using this vocabulary type, we can still use value semantics, but now our states and events can contain much more data and transfer it throughout the system. This allows us to encapsulate code better and make it simpler to reason.

Would you like to see more?
I have another article showing a shopping cart FSM example: FSM with std::variant and C++20 - Shopping Cart and it's available for C++ Stories Premium/Patreon members. See all Premium benefits here.

The code for the article can be found on my Github:

github.com/fenbf/articles/cpp20/stateMachine/stateMachine.cpp

(it also uses Modules and std::format where possible :))

Back to you

  • Do you use Finite State Machines in your code?
  • What techniques do you use to implement them?
  • Have you tried std::variant?

Share your comments below.