Close Menu
    Ciekawe

    Jak zabezpieczyć pendrive hasłem bez dodatkowych programów?

    2025-11-13

    Ile kosztuje prowadzenie jednoosobowej działalności gospodarczej? Przegląd opłat

    2025-11-10

    Acer czy Asus – który laptop wybrać? Porównanie i porady

    2025-11-05
    Facebook X (Twitter) Instagram
    CPP Polska
    Facebook X (Twitter) Instagram
    • Biznes

      Ile kosztuje prowadzenie jednoosobowej działalności gospodarczej? Przegląd opłat

      2025-11-10

      Jak wziąć samochód w leasing bez firmy? Poradnik dla osób fizycznych

      2025-10-29

      Jak założyć firmę jednoosobową krok po kroku – koszty, formalności i czas trwania

      2025-10-23

      Ile kosztuje stworzenie strony internetowej dla firmy? Cennik i porady

      2025-10-07

      Jak usunąć profil firmy z Google i Facebooka? Instrukcja krok po kroku

      2025-10-07
    • Technologie

      Jak zabezpieczyć pendrive hasłem bez dodatkowych programów?

      2025-11-13

      Acer czy Asus – który laptop wybrać? Porównanie i porady

      2025-11-05

      Jak przenieść okno na drugi monitor? Skróty i metody dla Windows i macOS

      2025-11-01

      Jak sprawdzić specyfikację laptopa? Pełna konfiguracja sprzętowa

      2025-10-26

      Co to jest VR? Wirtualna rzeczywistość i jej zastosowania

      2025-10-20
    • Programowanie

      Maszyna stanów oparta o std::variant

      2025-10-07

      Tablice w C++ od podstaw – deklaracja, inicjalizacja, iteracja i typowe pułapki

      2025-10-07

      std::deque w C++ – kiedy wybrać dwukierunkową kolejkę zamiast vectora

      2025-10-07

      itoa i std::to_chars – konwersja liczb na tekst bez narzutu wydajności

      2025-10-07

      strcpy vs strncpy vs std::string – bezpieczne kopiowanie łańcuchów w C++

      2025-10-07
    • Inne

      Jak prowadzić blog programistyczny i dzielić się wiedzą?

      2025-06-28
    CPP Polska
    Home»Programowanie»Maszyna stanów oparta o std::variant
    Programowanie

    Maszyna stanów oparta o std::variant

    Oskar KlimkiewiczBy Oskar KlimkiewiczBrak komentarzy8 Mins Read
    Share Facebook Twitter LinkedIn Email Copy Link
    Follow Us
    RSS
    turned on laptop computer
    Share
    Facebook Twitter LinkedIn Email Copy Link

    Comprehensive implementation of finite state machines using std::variant in modern C++

    Maszyny stanów skończonych (FSM) stanowią fundamentalny paradygmat w inżynierii oprogramowania do modelowania zachowań w systemach, począwszy od sterowników wbudowanych po sztuczną inteligencję w grach. Wprowadzenie std::variant w C++17 zrewolucjonizowało implementację FSM poprzez umożliwienie typowo-bezpiecznej reprezentacji stanów, semantyki wartościowej i rozwiązań bezalokacyjnych (heap-free), które przewyższają tradycyjne podejścia obiektowe. W poniższym artykule omawiamy podstawy techniczne, wzorce implementacyjne oraz praktyczne zastosowania FSM z użyciem std::variant poprzez liczne przykłady kodu i analizę wydajności.

    Theoretical foundations of finite state machines

    Computational model and classifications

    Maszyny stanów skończonych są abstrakcjami matematycznymi opisanymi przez 5-tkę $(Q, \Sigma, \delta, q_0, F)$, gdzie:

    • $Q$ reprezentuje skończony zbiór stanów,
    • $\Sigma$ oznacza alfabet wejściowy (zdarzenia),
    • $\delta : Q \times \Sigma \rightarrow Q$ to funkcja przejść,
    • $q_0 \in Q$ to stan początkowy,
    • $F \subseteq Q$ definiuje stany akceptujące.

    W implementacjach sprzętowych i programowych dominują dwa główne modele:

    1. Maszyny Moore’a – Wyjście zależy wyłącznie od bieżącego stanu; formalnie: $\lambda : Q \rightarrow \Gamma$, gdzie $\Gamma$ to alfabet wyjściowy;
    2. Maszyny Mealy’ego – Wyjście zależy zarówno od bieżącego stanu, jak i wejścia: $\lambda : Q \times \Sigma \rightarrow \Gamma$.

    Podejście z std::variant elegancko obsługuje oba modele dzięki składaniu typów na etapie kompilacji, eliminując narzut polimorfizmu czasu wykonania przy zachowaniu matematycznej ścisłości.

    Limitations of traditional implementations

    Tradycyjne implementacje FSM w C++ zwykle wykorzystują:

    • Enum/switch – Stają się nieczytelne wraz ze wzrostem liczby stanów/zdarzeń;
    • Wirtualną dziedziczenie – Wymaga alokacji na stercie i utrudnia semantykę wartościową;
    • Wzorzec stanu – Wprowadza pośrednictwo przez wywołania wirtualne.

    Pomiar czasu działania pokazuje, że FSM bazujące na wywołaniach wirtualnych są o 15–20% wolniejsze niż podejścia z std::variant w systemach o wysokiej częstotliwości działania. Ponadto tradycyjne projekty nie pozwalają efektywnie wykorzystać semantyki przenoszenia (move semantics), co wprowadza zbędne kopiowanie podczas przejść między stanami.

    std::variant technical fundamentals

    Type-safe state representation

    Najważniejszą innowację stanowi kodowanie stanów jako różne typy wewnątrz wariantu:

    namespace states { struct Idle { /* no data */ }; struct Processing { std::chrono::milliseconds elapsed; JobID current_job; }; struct Terminated { ErrorCode last_error; }; } using State = std::variant<states::Idle, states::Processing, states::Terminated>;

    Taka konstrukcja przechowuje dane specyficzne dla stanu bezpośrednio w typach stanów, czyniąc nielegalne kombinacje stanów niereprezentowalnymi. Przykładowo, stan Processing wymusza obecność danych o czasie i zadaniu, zaś Idle nie zawiera zbędnych pól.

    Value semantics and transition mechanics

    Przejścia stanów sprowadzają się do przypisania wartości:

    State current = states::Idle{}; // Zdarzenie powoduje przejście current = states::Processing{0ms, job123};

    Przypisanie to:

    1. Wywołuje destruktor poprzedniego obiektu stanu;
    2. Tworzy nowy stan „na miejscu”;
    3. Zachowuje bezpieczeństwo wyjątków dzięki gwarancjom przypisania wariantu.

    Event handling with std::visit

    Wzorzec przeciążenia funkcji umożliwia elegancką dyspozycję zdarzeń:

    template<class... Ts> struct overload : Ts... { using Ts::operator()...; }; template<class... Ts> overload(Ts...) -> overload<Ts...>; std::visit(overload{ [](states::Idle& s, const StartEvent& e) -> State { return states::Processing{0ms, e.job}; }, [](states::Processing& s, const CancelEvent& e) -> State { return states::Terminated{ErrorCode::Cancelled}; }, // ... inne obsługi zdarzeń }, current_state, event);

    Każda lambda obsługuje konkretne przejście typowane, a kompilator wymusza pełne pokrycie przypadków dzięki regułom odwiedzania wariantu.

    Implementation patterns

    Basic FSM structure

    Kompletna maszyna wymaga trzech głównych komponentów:

    // 1. Definicje stanów namespace states { struct Off { }; struct On { uint32_t brightness = 100; }; } // 2. Definicje zdarzeń namespace events { struct Toggle { }; struct AdjustBrightness { int32_t delta; }; } // 3. Logika FSM class LightFSM { using State = std::variant<states::Off, states::On>; State current_state = states::Off{}; public: template<typename Event> void dispatch(const Event& e) { std::visit([this, &e](auto& state) { current_state = transition(state, e); }, current_state); } private: State transition(states::Off&, const events::Toggle&) { return states::On{}; } State transition(states::On&, const events::Toggle&) { return states::Off{}; } State transition(states::On& s, const events::AdjustBrightness& e) { return states::On{std::clamp(s.brightness + e.delta, 0, 100)}; } };

    Prezentowany przykład pokazuje:

    • Stany i zdarzenia jako proste struktury (z parametrami lub bez);
    • Obsługę przejść realizowaną jako przeciążone metody;
    • Niezmienne przejścia – każda zmiana zwraca nową instancję stanu.

    Nested state machines

    Złożone systemy dekomponuje się na zagnieżdżone FSM:

    struct MotorController { struct Stopped { }; struct Running { FSM<states::Accelerating, states::Cruising> phase; }; // ... inne stany void onEmergencyStop() { if(std::holds_alternative<Running>(current_state)) { // Dostęp do zagnieżdżonego FSM auto& running = std::get<Running>(current_state); running.phase.dispatch(events::Stop{}); } current_state = states::Stopped{}; } };

    Warianty naturalnie obsługują kompozycję przez zagnieżdżanie stanów.

    Guard conditions and transition actions

    Realistyczne przejścia wymuszają walidację i efekty uboczne:

    State transition(states::Active& s, const events::Shutdown& e) { if(s.unsaved_work > 0) { triggerAutosave(); return states::Saving{ s.unsaved_work }; } return states::Terminated{}; }

    Warunki zaporowe pojawiają się jako prewarunki, zaś akcje są wykonywane przed zmianą stanu.

    Performance and optimization

    Memory layout characteristics

    Wariantowa konstrukcja posiada optymalne cechy pamięciowe:

    Podejście Rozmiar stanu Alokacja Koszt dostępu
    Bazowy wirtualny 1 wskaźnik Sterta (każdy stan) Wywołanie pośrednie + miss w cache
    std::variant maxstatesize Stos/statycznie Sprawdzenie indeksu + dostęp bezpośredni
    Union maxstatesize Stos Ręczne śledzenie typu

    Pomiary empiryczne wykazują:

    • Brak alokacji na stercie podczas przejść między stanami,
    • 2–5 razy szybsze przejścia w porównaniu do wywołań wirtualnych,
    • Przyjazny dla cache’a dostęp dzięki spójnej lokalizacji danych.

    Compile-time efficiency

    Realizacje silnie oparte na szablonach wydłużają czas kompilacji:

    • Ograniczanie eksplozji stanów – stosowanie wymazywania typów dla rzadkich stanów,
    • Pamiętanie przejść – cache’owanie obiektów odwiedzających,
    • Hermetyzacja modułów – izolacja FSM w osobnych modułach.

    Comparative analysis

    Versus traditional state pattern

    Rozważ kontroler stanu automatu sprzedającego:

    // Tradycyjne podejście OOP class VendingState { public: virtual void insertCoin(int) = 0; virtual void selectItem(string) = 0; // ...inne zdarzenia protected: VendingContext& context; }; class IdleState : public VendingState { void insertCoin(int amount) override { context.balance += amount; context.setState(new EnteredAmountState()); } // ... }; // Podejście std::variant namespace states { struct Idle { void insertCoin(int amount, VendingContext& ctx) { ctx.balance += amount; return states::AmountEntered{ctx.balance}; } }; }

    Najważniejsze różnice:

    • Brak alokacji na stercie – Stany przechowywane wewnątrz instancji wariantu;
    • Silna enkapsulacja – Dane specyficzne dla stanu są izolowane;
    • Lepsze debugowanie – Jawny typ stanu w debugerze.

    Limitations and workarounds

    Wybrane wyzwania:

    1. Współdzielenie danych między stanami – Rozwiązane przez referencje do obiektu kontekstu;
    2. Historia przejść – Realizowana przez migawki stanu;
    3. Rozproszone FSM – Wymagają proxy serializacyjnego.

    Real-world implementations

    Game character AI

    Trójstanowy kontroler gracza ilustruje praktyczne użycie:

    struct PlayerState { struct Normal { Vec3 position; Quaternion rotation; }; struct Jumping { Vec3 launch_velocity; float air_time = 0.0f; }; struct Dead { float respawn_timer; }; }; class CharacterFSM { std::variant<Normal, Jumping, Dead> state; PhysicsBody& body; public: void update(float dt) { std::visit([dt, this](auto& s) { if constexpr(requires { s.apply_physics; }) { body.applyForce(s.calculate_gravity()); } // Logika typowa dla stanu }, state); } void onCollision(const CollisionEvent& e) { if(e.impact > DEATH_THRESHOLD) { state = Dead{RESPAWN_DELAY}; playDeathAnimation(); } } };

    Przykład pokazuje:

    • Specyficzną fizykę dla stanu dzięki if constexpr;
    • Bezpośredni dostęp do danych bez pośrednictwa wskaźników;
    • Płynną integrację z kontekstem.

    Industrial control system

    Dla systemów czasu rzeczywistego deterministyczność działania jest kluczowa:

    template<typename Context> class ControlFSM { std::variant<states::Init, states::Running, states::Fault> state; Context& ctx; // Transition table na etapie kompilacji auto get_transition(states::Init&, const events::Ready&) { ctx.initializeSensors(); return states::Running{}; } public: void process(const auto& event) noexcept { static_assert(noexcept(transition(event)), "Transitions must be noexcept"); state = std::visit([&](auto& s) -> State { return transition(s, event); }, state); } };

    Najważniejsze cechy czasu rzeczywistego:

    • Gwarancje noexcept dla przewidywalności czasowej,
    • Walidacja przejść na etapie kompilacji,
    • Brak dynamicznych alokacji.

    Advanced techniques

    State machine metaprogramming

    Automatyczne generowanie FSM przez szablony umożliwia wielokrotne użycie wzorców:

    template<typename... States> class FSM { std::tuple<States...> state_objects; std::variant<States*...> current; public: template<typename State> void transition_to() { current = &std::get<State>(state_objects); } template<typename Event> void dispatch(const Event& e) { std::visit([&e](auto* s) { s->handle(e); }, current); } }; // Użycie FSM<Idle, Active, Error> fsm; fsm.transition_to<Active>();

    Taki wzorzec:

    • Centralizuje przechowywanie stanów w krotce,
    • Minimalizuje narzut tworzenia stanów,
    • Umożliwia wielokrotne użycie stanów pomiędzy instancjami.

    Transition visualization

    Introspekcja typów umożliwia tworzenie diagramów stanów w czasie działania:

    std::string state_name = std::visit(overload{ [](const states::Idle&) { return "Idle"; }, [](const states::Active&) { return "Active"; }, // ... }, current_state); std::cout << "Obecny stan: " << state_name;

    Połączona z logowaniem zdarzeń, technika ta pozwala na monitorowanie FSM w aplikacji.

    Conclusion and recommendations

    FSM w oparciu o std::variant stanowią przełom w projektowaniu maszyn stanów skończonych w nowoczesnym C++, oferując znaczące korzyści w zakresie bezpieczeństwa typów, wydajności podczas działania i czytelności kodu w porównaniu do tradycyjnych rozwiązań. Przedstawione tu wzorce i przykłady pozwalają inżynierom wdrażać niezawodne systemy zarządzania stanem w dziedzinach od programowania gier po automatykę przemysłową.

    Best practice recommendations

    1. Preferuj semantykę wartościową – projektuj stany jako typy trywialne, przenośne;
    2. Wykorzystuj constexpr – walidacja stanów podczas kompilacji;
    3. Izoluj efekty uboczne – obiekt kontekstu dla operacji nieczystych;
    4. Implementuj historię – opcjonalne migawki stanu do debugowania;
    5. Profiluj przejścia – monitoruj kluczowe ścieżki FSM.

    Ewolucja C++ stale zwiększa możliwości FSM, a propozycje pattern matchingu w C++23 zapowiadają jeszcze zwięźlejszą składnię. W przyszłości warto badać podejścia hybrydowe łączące std::variant z modelami równoległego działania dla rozproszonych systemów stanowych.

    // Kompleksowy framework FSM, łączący opisane techniki template<typename Context, typename... States> class AdvancedFSM { Context ctx; std::variant<States...> current_state; std::vector<StateSnapshot> history; public: template<typename Event> void dispatch(const Event& e) { auto new_state = std::visit([&](auto& s) { if constexpr(requires { s.handle(e, ctx); }) { return s.handle(e, ctx); } else { return s; // Brak przejścia } }, current_state); if(!std::holds_same_type(current_state, new_state)) { history.push_back({typeid(current_state).name(), typeid(new_state).name(), std::chrono::system_clock::now()}); current_state = std::move(new_state); } } };

    Polecane:

    • enum class w C++ – typy wyliczeniowe ze scopem, operatory bitowe i serializacja
    • Zaawansowane scenariusze z std::visit i wieloma wariantami
    • Czym jest std::variant i kiedy go stosować
    • sort w C++ – sortowanie kontenerów STL, własne komparatory i lambda-komparatory
    • fstream w C++ – czytanie i zapis plików tekstowych oraz binarnych z obsługą błędów
    Share. Facebook Twitter LinkedIn Email Copy Link
    Oskar Klimkiewicz
    • Website

    Inżynier oprogramowania specjalizujący się w C++, absolwent Wydziału Elektroniki i Technik Informacyjnych Politechniki Warszawskiej. Od ponad 8 lat projektuje i rozwija systemy o wysokiej dostępności, głównie dla branży fintech i IoT. PS. Zdjęcie wyretuszowane przez AI :)

    Podobne artykuły

    Tablice w C++ od podstaw – deklaracja, inicjalizacja, iteracja i typowe pułapki

    4 Mins Read

    std::deque w C++ – kiedy wybrać dwukierunkową kolejkę zamiast vectora

    4 Mins Read

    itoa i std::to_chars – konwersja liczb na tekst bez narzutu wydajności

    5 Mins Read
    Leave A Reply Cancel Reply

    Oglądaj, słuchaj, ćwicz - zdobywaj nowe umiejętności online
    Nie przegap

    Jak zabezpieczyć pendrive hasłem bez dodatkowych programów?

    Oskar Klimkiewicz5 Mins Read

    Zabezpieczenie danych na przenośnych nośnikach USB jest kluczowe we współczesnym środowisku cyfrowym, gdzie zagrożenia cybernetyczne…

    Ile kosztuje prowadzenie jednoosobowej działalności gospodarczej? Przegląd opłat

    2025-11-10

    Acer czy Asus – który laptop wybrać? Porównanie i porady

    2025-11-05

    Jak przenieść okno na drugi monitor? Skróty i metody dla Windows i macOS

    2025-11-01
    Social media
    • Facebook
    • Twitter
    • LinkedIn
    O nas
    O nas

    CPP Polska to serwis internetowy poświęcony technologii, programowaniu, IT, biznesowi i finansom. Znajdziesz tu porady, wskazówki i instrukcje dla wszystkich czytelników IT & Tech & Biz.

    Facebook X (Twitter) LinkedIn RSS
    Najnowsze

    Jak zabezpieczyć pendrive hasłem bez dodatkowych programów?

    2025-11-13

    Ile kosztuje prowadzenie jednoosobowej działalności gospodarczej? Przegląd opłat

    2025-11-10

    Acer czy Asus – który laptop wybrać? Porównanie i porady

    2025-11-05
    Popularne

    Skrajnie niepotrzebne, skrajne przypadki w C++

    2025-06-28

    Wyszukiwanie testów w Google Test – metody i narzędzia

    2025-06-28

    Czy C jest wolniejszy od C++? Zero-cost abstraction w praktyce

    2025-06-28
    © 2025 CPP Polska. Wszelkie prawa zastrzeżone.
    • Lista publikacji
    • Współpraca
    • Kontakt

    Type above and press Enter to search. Press Esc to cancel.