Last Update:
Finite State Machine with std::variant - Vending Machine
Table of Contents
In my last article, we discussed Finite State Machines based on std::variant
and some cool C++17 techniques. Today I want to go further and show you an example of a Vending Machine implementation.
Here’s the previous article Finite State Machine with std::variant - C++ Stories
States & Events
Here’s a basic diagram that illustrates how the machine is going to work:
Following the model from the last article, each state and the event will be a separate small structure. Later we can “pack” them into std::variant
.
Here are the states:
namespace state {
struct Idle { };
struct AmountEntered { int amount{ 0 }; int availableChange{ 0 }; };
struct ItemSelected { std::string item; int availableChange{ 0 }; };
struct ChangeDispensed { int change{ 0 }; };
}
using VendingState = std::variant<state::Idle,
state::AmountEntered,
state::ItemSelected,
state::ChangeDispensed>;
And the events:
namespace event {
struct EnterAmount { int amount{ 0 }; };
struct SelectItem { std::string item; };
struct DispenseChange { };
struct Reset { };
}
using PossibleEvent = std::variant<event::EnterAmount,
event::SelectItem,
event::DispenseChange,
event::Reset>;
Nothing fancy here, but notice that we’re very flexible thanks to small structures. If you encoded this as enum
values, you’d have issues storing extra members along with the states.
Here’s a possible attempt with enum
:
enum class VendingState {
Idle,
AmountEntered,
ItemSelected,
ChangeDispensed
};
enum class PossibleEvent {
EnterAmount,
SelectItem,
DispenseChange,
Reset
};
struct Event {
PossibleEvent type;
int amount;
std::string item;
};
The Event
struct has to be “generic” and holds amount
and item
, which might be used differently for different events. While the code could be easier to read, it’s less type-safe.
The Machine
The machine has the following core part:
class VendingMachine {
struct Item {
std::string name;
unsigned quantity { 0 };
int price {0};
};
public:
void processEvent(const PossibleEvent& event) {
state_ = std::visit(helper::overload{
[this](const auto& state, const auto& evt) {
return onEvent(state, evt);
}
}, state_, event);
}
// on event overloads.
void reportCurrentState();
void reportRegistry();
private:
std::vector<Item> registry_ {
Item {"Coke", 5, 50},
Item {"Pepsi", 3, 45},
Item {"Water", 4, 35},
Item {"Snack", 5, 25}
};
VendingState state_;
};
The class holds registry_
with available items. My demo implementation is very simple, but you can easily extend it and allow for more customization and control.
processEvent
is the primary member function that accepts an event and propagates it onto the states and proper OnEvent
overloads. Let’s have a look at them now.
Events
We have the following events:
VendingState onEvent(const state::Idle& idle,
const event::EnterAmount& amount);
VendingState onEvent(const state::AmountEntered& current,
const event::EnterAmount& amount);
VendingState onEvent(const state::AmountEntered& amountEntered,
const event::SelectItem& item);
VendingState onEvent(const state::ItemSelected& itemSelected,
const event::DispenseChange& change);
VendingState onEvent(const state::ChangeDispensed& changeDispensed,
const event::Reset& reset);
// just in case...
VendingState onEvent(const auto&, const auto&);
Most of them are straightforward, but let’s have a look at the event that selects the item:
VendingState onEvent(const state::AmountEntered& amountEntered, const event::SelectItem& item) {
std::cout << std::format("AmountEntered {} -> SelectItem: {}\n", amountEntered.amount, item.item);
auto it = std::ranges::find(registry_, item.item, &Item::name);
if (it == registry_.end())
throw std::logic_error{ "Item not found in item registry." };
if (it->quantity > 0 && it->price <= amountEntered.amount)
{
std::cout << "item found...\n";
--(it->quantity);
return state::ItemSelected{ item.item, amountEntered.availableChange - it->price };
}
return amountEntered;
}
Because processEvent
is a member function creating overload
with this
, we can safely access all members of the VendingMachine
object. This is quite handy, and, as you can see, we can quickly check the availability of items in the registry_
.
Demo
Here’s one flow of the states:
void VendingMachineTest() {
VendingMachine vm;
vm.reportRegistry();
try {
vm.processEvent(event::EnterAmount{ 30 });
vm.processEvent(event::EnterAmount{ 30 });
//vm.reportCurrentState();
vm.processEvent(event::SelectItem{ "Coke" });
vm.reportRegistry();
//vm.reportCurrentState();
vm.processEvent(event::DispenseChange{});
//vm.reportCurrentState();
vm.processEvent(event::Reset{ });
//vm.reportCurrentState();
}
catch (std::exception& ex) {
std::cout << "Exception! " << ex.what() << '\n';
}
}
Run at Compiler Explorer
And here’s the output:
available items:
Coke, price 50, quantity 5
Pepsi, price 45, quantity 3
Water, price 35, quantity 4
Snack, price 25, quantity 5
Idle -> EnterAmount: 30
AmountEntered 30 -> EnterAmount: 30
AmountEntered 60 -> SelectItem: Coke
item found...
available items:
Coke, price 50, quantity 4
Pepsi, price 45, quantity 3
Water, price 35, quantity 4
Snack, price 25, quantity 5
ItemSelected -> DispenseChange 10
ChangeDispensed -> Reset
The output reflects the steps of the VendingMachineTest
:
- The vending machine displays its initial inventory, including the item name, price, and quantity.
- A customer enters an amount of 30 twice, increasing the total entered amount to 60.
- The customer selects a Coke, which costs 50, and the machine verifies the item’s availability and affordability.
- The machine updates the inventory, showing that the quantity of Coke has decreased from 5 to 4.
- The machine dispenses the remaining balance (10) to the customer after the purchase.
- The machine resets to the idle state, ready for the next transaction.
Summary
In this article, we explored a potential and straightforward way to create a Vending Machine using the std::variant
technique for states and events. What’s fascinating is that the onEvent
functions can be part of the primary class, which enables access to all data members of the enclosing object.
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. The example also shows an optimization for processEvent()
and a way to talk to other objects (ordering system). It's available for C++ Stories Premium/Patreon members.
See all Premium benefits here.
Some extensions and updates:
- The event which returns coins before the item is selected is not implemented.
- We could add a timer that would reset the machine and return the money when the user doesn’t select any item.
- Better error handling
- Better items management, ensuring items are present
I’ll leave it as homework :)
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: