The Problem with C++ State Machines
Implementing state machines in C++ typically involves a choice between simplicity and correctness. Manual approaches using switch-case statements start simple but become unmaintainable as states and transitions grow. Framework-based solutions like Boost.Statechart or Qt State Machine add runtime overhead and force your code into specific class hierarchies.
Common approaches and their trade-offs
| Approach | Pros | Cons |
|---|---|---|
| switch-case / enum | Simple, no dependencies | No hierarchy, hard to maintain, no formal verification |
| Boost.Statechart | Full UML statechart support | Heavy template usage, slow compilation, virtual dispatch |
| Boost.MSM | Compile-time transitions | Complex MPL syntax, long compile times, steep learning curve |
| Qt State Machine | Integrates with Qt event loop | Requires Qt dependency, QObject overhead, runtime construction |
| SCE (AOT) | Zero overhead, W3C standard, compile-time enums | Requires build-time code generation step |
How SCE Works
SCE takes a different approach: you define your state machine in W3C SCXML (an XML-based standard for statecharts), and SCE's code generator produces a self-contained C++ header file at build time. The generated code uses compile-time enums for states and events, with no virtual functions, no RTTI, and no dynamic allocation.
Step 1: Define your state machine in SCXML
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0"
name="TrafficLight" initial="red">
<state id="red">
<transition event="timer" target="green"/>
</state>
<state id="green">
<transition event="timer" target="yellow"/>
</state>
<state id="yellow">
<transition event="timer" target="red"/>
</state>
</scxml>
Step 2: Generate C++ code
# Command line
sce-codegen generate traffic_light.scxml -o ./generated/ -l cpp
# Or with CMake (automatic)
sce_add_state_machine(TARGET my_app SCXML_FILE traffic_light.scxml)
Step 3: Use the generated state machine
#include "traffic_light_sm.h"
#include "wrappers/AutoProcessStateMachine.h"
int main() {
using namespace SCE::Generated::traffic_light;
SCE::Wrappers::AutoProcessStateMachine<traffic_light> light;
light.initialize(); // Enters "red" state
light.processEvent(Event::Timer); // red -> green
light.processEvent(Event::Timer); // green -> yellow
light.processEvent(Event::Timer); // yellow -> red
}
The generated header contains enum classes for State and Event, a compact transition table, and inline action handlers. No base class required, no framework lock-in. The state machine object starts at 8 bytes of memory.
C++ Function Integration
SCE allows you to call your C++ functions directly from SCXML transitions and actions using a Named Context pattern. Your business logic code remains completely independent of the state machine framework.
SCXML with Named Context
<!-- Declare Named Context: bind "hardware" to your C++ type -->
<scxml xmlns="http://www.w3.org/2005/07/scxml"
xmlns:sce="urn:sce:extensions" xmlns:cpp="urn:sce:cpp"
version="1.0" name="SmartLight" initial="off">
<sce:context id="hardware" cpp:type="Hardware" cpp:include="hardware.h"/>
<state id="off">
<onentry>
<script><cpp>hardware.powerOff()</cpp></script>
</onentry>
<transition event="switch_on" cond="cpp:hardware.hasPower()" target="on">
<script><cpp>hardware.powerOn()</cpp></script>
</transition>
</state>
<state id="on">
<onentry>
<script><cpp>hardware.setBrightness(100)</cpp></script>
</onentry>
<transition event="switch_off" target="off"/>
</state>
</scxml>
Your C++ code (no framework dependency)
// hardware.h -- your business logic, completely independent
struct Hardware {
bool hasPower() { return true; }
void powerOn() { /* ... */ }
void powerOff() { /* ... */ }
void setBrightness(int level) { /* ... */ }
};
Usage: Named Context is passed directly (no wrapper struct)
#include "smart_light_sm.h"
int main() {
using namespace SCE::Generated::smart_light;
// Named Context: pass your type directly as template parameter
Hardware hardware;
smart_light light(hardware); // Dependency injection via constructor
light.initialize();
light.raiseExternal(Event::Switch_on);
light.step(); // off -> on (calls hardware.powerOn())
light.raiseExternal(Event::Switch_off);
light.step(); // on -> off (calls hardware.powerOff())
}
The <sce:context> declaration generates a templated state
machine class. Your type is the template parameter -- no wrapper struct
needed. The generated code accesses your object via
this->user_->hardware.powerOff(), providing
zero-overhead direct calls with no virtual dispatch.
Platform Integration
SCE provides event dispatchers for the most common C++ application frameworks, enabling asynchronous state machine event processing:
| Dispatcher | Platform | Timer Source |
|---|---|---|
| StdThreadDispatcher | Any C++11 | std::condition_variable |
| QtDispatcher | Qt5 / Qt6 | QTimer |
| GLibDispatcher | GTK / GNOME | GSource |
#include "dispatchers/StdThreadDispatcher.h"
#include "wrappers/AsyncStateMachine.h"
auto dispatcher = StdThreadDispatcher::create();
AsyncStateMachine<traffic_light, Event> sm(dispatcher);
sm.initialize();
dispatcher->start();
// Post events from any thread (thread-safe)
sm.postEvent(Event::Timer);
dispatcher->stop();
Build System Integration
SCE integrates with CMake via three methods:
- find_package -- for production projects with SCE installed system-wide
- add_subdirectory -- for projects including SCE as a git submodule
- FetchContent -- for automatic dependency management
# FetchContent example (simplest)
include(FetchContent)
FetchContent_Declare(sce
GIT_REPOSITORY https://github.com/newmassrael/scxml-core-engine.git
GIT_TAG main)
FetchContent_MakeAvailable(sce)
add_executable(my_app main.cpp)
sce_add_state_machine(TARGET my_app SCXML_FILE my_machine.scxml)
target_link_libraries(my_app PRIVATE sce_runtime)
4-Tier Library Architecture
SCE's C++ library is split into four tiers so you can link only what you need:
| Tier | Dependencies | Use Case |
|---|---|---|
sce_core |
Header-only, zero | Pure static AOT (embedded, microcontrollers) |
sce_base |
+ logger | AOT with logging |
sce_scripting |
+ Lua/QuickJS | Static hybrid (ECMAScript expressions) |
sce_runtime |
+ XML parser | Full interpreter (runtime SCXML loading) |
For most embedded use cases, sce_core (header-only, zero
dependencies) is sufficient. The code generator automatically selects
the optimal tier based on the SCXML features used.
Performance
- 8 bytes minimal state machine footprint (verified via benchmark)
- Sub-microsecond state transitions in pure static mode (Release build)
- Low-latency async event enqueue via lock-free design
- <1 ms timer drift for 100ms intervals
- Zero virtual functions in pure static mode
- C++17 minimum (C++20 for full runtime features)
W3C SCXML Compliance
SCE passes all 202 mandatory W3C SCXML 1.0 conformance tests, covering compound states, parallel states, history states, invoke, delayed events, and ECMAScript datamodels. The code generator automatically detects which features your SCXML uses and selects the optimal engine:
- Pure Static -- no ECMAScript, zero runtime overhead
- Static Hybrid -- ECMAScript present, minimal overhead via script engine
- Interpreter -- only when SCXML content is loaded at runtime