본문으로 건너뛰기
Embedded C++ for Real Systems · 35/41

Compile-time FSM 구현 — 템플릿으로 상태 전이 검증

· Hawk · 4분 읽기

#한 줄 요약

“FSM을 컴파일 타임 table로 표현합니다.” invalid 전이는 빌드 실패로 잡히고 런타임 코드는 최소가 됩니다.

#어떤 문제를 푸는가

Part 4-06의 런타임 FSM은 유연하지만 다음과 같은 단점이 있습니다.

  • Invalid 전이가 런타임에야 발견됩니다.
  • 전이 table이 메모리에 올라갑니다.
  • 디버깅 시 stack trace가 복잡합니다.

Compile-time FSMconstexpr과 template을 활용해 다음을 얻습니다.

  • 전이 검증을 컴파일 타임에 합니다.
  • invalid 전이는 빌드 실패로 잡힙니다.
  • 각 전이가 컴파일러에 의해 인라인됩니다.
constexpr auto next = transition<State::Idle, Event::Start>(); // 컴파일 타임
// next = State::Running
constexpr auto bad = transition<State::Idle, Event::Stop>(); // 컴파일 에러
// error: invalid transition

런타임 코드와 데이터 모두 0이며 수 KB를 절약할 수 있습니다.

#기본 — Transition table as data

enum class State : uint8_t { Idle, Running, Paused, Stopped };
enum class Event : uint8_t { Start, Pause, Resume, Stop };
struct Transition {
State from;
Event event;
State to;
};
constexpr Transition transitions[] = {
{State::Idle, Event::Start, State::Running},
{State::Running, Event::Pause, State::Paused},
{State::Running, Event::Stop, State::Stopped},
{State::Paused, Event::Resume, State::Running},
{State::Paused, Event::Stop, State::Stopped},
};
constexpr State next_state(State current, Event event) {
for (const auto& t : transitions) {
if (t.from == current && t.event == event) return t.to;
}
return current; // no transition
}
// 런타임 또는 컴파일 타임
constexpr State s = next_state(State::Idle, Event::Start);
static_assert(s == State::Running);

table은 .rodata에 들어가고, next_state는 컴파일 타임에도 호출할 수 있습니다.

#Template-based — invalid 전이 컴파일 에러

template<State From, Event Ev>
struct TransitionT {
static constexpr bool valid = false;
static constexpr State to = From; // no change
};
// 명시적 specialization으로 valid 전이 정의
template<> struct TransitionT<State::Idle, Event::Start> {
static constexpr bool valid = true;
static constexpr State to = State::Running;
};
template<> struct TransitionT<State::Running, Event::Pause> {
static constexpr bool valid = true;
static constexpr State to = State::Paused;
};
template<> struct TransitionT<State::Running, Event::Stop> {
static constexpr bool valid = true;
static constexpr State to = State::Stopped;
};
// ... 모든 valid 전이
// 사용
template<State From, Event Ev>
constexpr State transition() {
static_assert(TransitionT<From, Ev>::valid,
"Invalid state transition");
return TransitionT<From, Ev>::to;
}
constexpr State s1 = transition<State::Idle, Event::Start>(); // OK
// constexpr State s2 = transition<State::Idle, Event::Stop>(); // 컴파일 에러

컴파일 타임에 검증되며 invalid 전이는 static_assert로 잡힙니다.

#Type-based state — 각 state가 type

Part 4-06의 variant 기반 접근입니다.

struct Idle {};
struct Running { uint32_t since; };
struct Paused { uint32_t since; };
struct Stopped {};
using State = std::variant<Idle, Running, Paused, Stopped>;
struct StartEvent {};
struct PauseEvent {};
struct ResumeEvent {};
struct StopEvent {};
// 전이 함수 — 각 (state, event) 조합
constexpr State transition(Idle, StartEvent) {
return Running{get_time()};
}
constexpr State transition(Running r, PauseEvent) {
return Paused{r.since};
}
constexpr State transition(Running, StopEvent) {
return Stopped{};
}
constexpr State transition(Paused p, ResumeEvent) {
return Running{p.since};
}
// catch-all — invalid transition은 현재 state 유지
template<typename S, typename E>
constexpr State transition(S s, E) {
static_assert(sizeof(E) == 0, "Invalid transition");
return s;
}
// 사용
template<typename E>
void send_event(State& current, E ev) {
current = std::visit([&ev](auto&& state) -> State {
return transition(state, ev);
}, current);
}
State machine = Idle{};
send_event(machine, StartEvent{}); // Idle → Running
// send_event(machine, StopEvent{}); // Idle + StopEvent — static_assert 실패

overload resolution이 valid transition만 찾고, 없으면 static_assert가 실패합니다.

#Boost.SML — 정식 compile-time FSM

Boost.SML은 정식 State Machine Library이며 DSL과 비슷한 syntax를 제공합니다.

#include <boost/sml.hpp>
namespace sml = boost::sml;
struct Start {};
struct Pause {};
struct Resume {};
struct Stop {};
struct Machine {
auto operator()() {
using namespace sml;
return make_transition_table(
*"Idle"_s + event<Start> / [](){ start_play(); } = "Running"_s,
"Running"_s + event<Pause> / [](){ pause_play(); } = "Paused"_s,
"Running"_s + event<Stop> / [](){ stop_play(); } = "Stopped"_s,
"Paused"_s + event<Resume> / [](){ resume_play(); } = "Running"_s,
"Paused"_s + event<Stop> / [](){ stop_play(); } = "Stopped"_s,
"Stopped"_s + event<Start> / [](){ start_play(); } = "Running"_s
);
}
};
sml::sm<Machine> fsm;
fsm.process_event(Start{});
fsm.process_event(Pause{});
fsm.process_event(Resume{});

장점은 다음과 같습니다.

  • DSL syntax가 UML state diagram에 가깝습니다.
  • 컴파일 타임에 검증됩니다.
  • guard, action, sub-state machine을 지원합니다.
  • meta-programming으로 최적화되어 거의 zero-cost입니다.

단점은 다음과 같습니다.

  • heavy template이라 컴파일 시간이 늘어납니다.
  • 학습 곡선이 있습니다.

임베디드에서 큰 FSM에 유용합니다. 작으면 직접 enum이나 variant를 씁니다.

#임베디드 — Compile-time HSM

Hierarchical State Machine을 template으로 만드는 방법입니다.

template<typename Parent = void>
struct StateBase {
using parent_type = Parent;
};
struct Idle : StateBase<> {};
struct Working : StateBase<> {};
struct Reading : StateBase<Working> {}; // Working의 substate
struct Writing : StateBase<Working> {};
template<typename State>
constexpr bool is_in_state(/* ... */) {
// ... 부모 상태 chain 추적
}

구현이 복잡하므로 Boost.SML이 대부분을 처리합니다. 직접 구현은 학습 목적에만 권합니다.

#Compile-time 검증 — Unreachable state

전이 table에서 도달 불가능한 state를 찾을 수 있습니다.

constexpr Transition trs[] = {
{State::Idle, Event::Start, State::Running},
{State::Running, Event::Stop, State::Stopped},
// State::Paused — 도달 불가능
};
constexpr bool is_reachable(State s) {
if (s == State::Idle) return true; // 시작 state
for (const auto& t : trs) {
if (t.to == s && is_reachable(t.from)) return true;
}
return false;
}
static_assert(is_reachable(State::Running));
static_assert(is_reachable(State::Stopped));
// static_assert(is_reachable(State::Paused)); // 컴파일 에러 — unreachable

컴파일 시점에 unreachable state를 발견할 수 있어 디자인을 검증할 수 있습니다.

#Compile-time 검증 — Dead-end state

빠져나갈 수 없는 dead state를 찾을 수도 있습니다.

constexpr bool has_outgoing(State s) {
for (const auto& t : trs) {
if (t.from == s) return true;
}
return false;
}
static_assert(has_outgoing(State::Running));
// static_assert(has_outgoing(State::Stopped)); // dead state

Stopped 같은 종착 state는 의도된 dead state이므로 명시합니다. 의도하지 않은 dead state는 bug입니다.

#자주 보는 함정과 안티패턴

#1. 모든 FSM을 compile-time으로

런타임 동적 변경이 필요한 시스템에서는 runtime FSM이 자연스럽습니다. compile-time은 trade-off가 있는 선택입니다.

#2. Template error message 폭증

잘못된 전이 시 에러가 수십 줄로 늘어납니다. static_assert 메시지를 명확히 작성합니다.

#3. Compile time 폭증

큰 FSM의 template instantiation은 컴파일 시간이 수 분에 이를 수도 있습니다. 측정이 필요합니다.

#4. Action 누락

전이마다 side effect(logging, action)가 따라옵니다. table에 함수 포인터나 action class를 함께 둡니다.

#5. Guard 누락

조건부 전이가 필요한 경우 단순한 from/event/to만으로는 부족합니다. Boost.SML이 guard를 지원합니다.

#6. Sub-machine 결합

큰 시스템에서는 sub-FSM이 필요해집니다. 직접 구현이 어렵다면 Boost.SML이나 etl::hsm을 씁니다.

#측정 — Compile vs Runtime FSM

같은 5-state FSM에서 1000 event를 처리한 결과입니다.

# Runtime FSM (enum + switch)
.text: 380 B
total cycles: 8000
# Variant + visit
.text: 720 B
total cycles: 9000 (variant copy + visit)
# Compile-time (template specialization)
.text: 180 B (대부분 인라인)
total cycles: 5000 (직접 dispatch)
# Boost.SML
.text: 920 B
total cycles: 7500

Compile-time이 가장 작고 빠르지만 유연성은 낮습니다.

#정리

  • Compile-time FSM은 constexpr table이나 template specialization으로 구현합니다.
  • Invalid 전이는 static_assert로 컴파일 에러를 냅니다.
  • Type-based state(variant)로 각 state의 데이터를 분리합니다.
  • Boost.SML이 정식 라이브러리이며 DSL syntax와 guard/action을 지원합니다.
  • Unreachable이나 dead state도 컴파일 타임에 검증할 수 있습니다.
  • 작은 FSM은 enum으로, 큰 FSM은 Boost.SML로 다룹니다.

#관련 항목

#다음 글

Part 4-08: Singleton 대안 — 임베디드의 DI 패턴. Singleton 없이 의존성을 명확히 합니다.

Embedded C++ for Real Systems · 36 of 41

  1. 1Embedded C++ for Real Systems — 임베디드 모던 C++ 시리즈 소개
  2. 2임베디드 C++ vs C — 런타임·코드 크기·ABI 관점 비교
  3. 3임베디드 C++ 컴파일러 플래그 분석 — -fno-rtti·-fno-exceptions·-Os
  4. 4임베디드 C++ 런타임 요구사항 — libstdc++·newlib·crt0 분석
  5. 5C++ 코드 크기 분석 — 가상 함수·템플릿·예외 비용 추적
  6. 6C++ ABI 호환성 — Itanium ABI·name mangling·vtable 레이아웃
  7. 7C++ 스타트업 코드 분석 — .init_array·전역 생성자 호출 순서
  8. 8임베디드 C++ 링커 스크립트 — vtable·정적 객체 배치 추적
  9. 9임베디드 C++ 표준 선택 가이드 — C++11/14/17/20/23 트레이드오프
  10. 10임베디드 RAII 기초 — 리소스 안전성과 결정적 소멸 보장
  11. 11임베디드 RAII 실전 패턴 — Lock·Pin·DMA·Power 관리
  12. 12constexpr 기초와 임베디드 적용 — 컴파일 타임 계산 활용
  13. 13constexpr 고급 활용 — 룩업 테이블·CRC·해시 컴파일 타임 생성
  14. 14consteval과 constinit 분석 — C++20 컴파일 타임 강제 메커니즘
  15. 15임베디드 Templates 기초 — 타입 안전과 코드 재사용 분석
  16. 16Template 비용 분석 — 코드 폭증·인스턴스화·디버그 정보 측정
  17. 17CRTP 패턴 분석 — vtable 없는 정적 다형성
  18. 18Type Traits 임베디드 활용 — SFINAE·is_pod·컴파일 타임 검사
  19. 19C++20 Concepts 활용 — 템플릿 제약과 가독성 개선
  20. 20동적 할당 없는 임베디드 C++ — placement new·정적 객체·풀
  21. 21Custom Allocator 기초 — std::allocator 인터페이스 분석
  22. 22Pool Allocator 구현 — Fixed-Size Block과 O(1) 보장
  23. 23std::pmr 임베디드 활용 — Polymorphic Memory Resource 분석
  24. 24No-Exception C++ 설계 — 코드 크기·결정성 트레이드오프
  25. 25임베디드 에러 처리 패턴 — Result·errno·optional 비교
  26. 26std::expected 분석 — C++23 결과 타입과 에러 전파
  27. 27No-RTTI C++ 설계 — dynamic_cast 제거와 정적 타입 분기
  28. 28임베디드 스마트 포인터 선택 — unique·shared·custom 비교
  29. 29임베디드 C++ 소유권 모델 — single·shared·borrow 패턴
  30. 30Intrusive Containers 분석 — 동적 할당 없는 컨테이너 설계
  31. 31ETL 라이브러리 분석 — Embedded Template Library의 STL 대체
  32. 32임베디드 Lock-free 기초 — atomic·memory ordering·CAS
  33. 33Lock-free Container 구현 — SPSC Queue·Ring Buffer
  34. 34Type-safe Flags 패턴 — Enum Class·Strong Typedef·Tag
  35. 35임베디드 State Machine 패턴 — Variant·Visitor·Table-driven 비교
  36. 36Compile-time FSM 구현 — 템플릿으로 상태 전이 검증
  37. 37Singleton 대안 패턴 — Service Locator·Static Init·Phantom
  38. 38MMIO Register 추상화 — 타입 안전한 비트 필드 접근
  39. 39GPIO 추상화 패턴 — Template·Concept으로 보드 독립성
  40. 40Peripheral 추상화 — UART·SPI·I2C 공통 인터페이스 설계
  41. 41임베디드 HAL 설계 패턴 — Static·Dynamic·Hybrid 비교