Надіслати запит Працюйте в Sii

Кінцеві автомати – це дуже корисна концепція, яка дозволяє моделювати складну поведінку. Основна їх ідея досить проста. У нас є набір можливих станів, і ми визначаємо правила, які керують переходами між поточним станом і певним іншим станом у випадку отримання інформації про подію. Як же це реалізувати у C++17? Звичайно, способів існує безліч. У цій статті ми спробуємо дослідити ще одну можливість з використанням кількох нових доповнень до стандарту C++.

Побудуємо автомат

То що ж таке стан? Насправді це може бути що завгодно. В контексті цієї статті просто припустимо, що це довільний об’єкт. Тип цього об’єкта можна використовувати для того, щоб відрізнити його від інших станів. Таким чином, нам не доведеться вести окремий список усіх можливих станів (наприклад, у вигляді переліку). Крім того, ми не хочемо запроваджувати будь-яку форму зв’язку між цими типами, щоб максимально їх розмежувати. Тож як передавати і зберігати ці абсолютно незалежні типи? У цьому нам може допомогти шаблон зі змінною кількістю аргументів, який містить std::tuple (обидва вони введені в C++11).

template <typename... States>
class StateMachine
{
private:
std::tuple<States...> states;
};

В цього класу є кілька особливостей, на які слід звернути увагу. По-перше, параметр StateMachine не має попередньої інформації про стани, які він містить. Крім того, нам не потрібно турбуватися про строк його існування, оскільки він прив’язаний до строку існування самого автомата.

Наступне у нашому списку справ – відстеження того, який стан вибрано в даний момент. Зазвичай для цього достатньо простого вказівника або посилання, але у нашому випадку це не спрацює, оскільки неможливо вибрати єдиний тип, який би приймав усі можливі типи станів (окрім void*, але він нам не підходить). Оскільки ми намагаємося зберігати різнорідні типи даних, ми можемо використати для цього std::variant (C++17). Оскільки std::variant не дозволяє використовувати його з посиланнями, нам слід використовувати звичайні вказівники. Крім того, ми припускаємо, що перший тип стану, яким наділяється автомат, є початковим станом.

std::variant<States*...> currentState { &std::get<0>(states) };

Тепер настав час додати метод для зміни поточного стану автомата. Знову ж таки, оскільки ми розрізняємо стани за типом, це має бути шаблонний метод.

template <typename State>
void transitionTo()
{
currentState = &std::get<State>(states);
}

Поки що все гаразд. Тепер нам потрібно передати автомату інформацію про подію. Тут виникає фундаментальне питання – які саме події ми хочемо обробляти? Оскільки стани виконують фактичну роботу, давайте просто передамо подію поточному стану і дозволимо йому її обробити. Іншими словами, автомат здатен приймати всі події, які його стани здатні обробити.

template <typename Event>
void handle(const Event& event)
{
auto passEventToState = [&event] (auto statePtr) {
statePtr->handle(event);
};
std::visit(passEventToState, currentState);
}

Ця зміна ввела залежність від типу стану. Кожного разу, коли автомат обробляє подію типу T, компілятор переконується, що всі типи станів мають метод «handle», який приймає подію типу T.

Останнє, чого не вистачає – це можливості переходу в інший стан на основі переданої події. Нам потрібно з’ясувати, як наказати автомату виконати перехід, перебуваючи всередині обробника події у заданому стані. Однак на шляху до цього є деякі перешкоди. Ми не хочемо передавати тип автомата станам, тому що не хочемо пов’язувати їх між собою. Щоб вирішити цю проблему, повернімо з методу «handle» стану проміжний об’єкт, котрий буде описувати, яку дію має виконати автомат.

template <typename State>
struct TransitionTo
{
template <typename Machine>
void execute(Machine& machine)
{
machine.template transitionTo<State>();
}
};

Формально кажучи, після отримання події автомат завжди повинен переходити у певний стан, навіть якщо він збігається з поточним. З іншого боку, бувають ситуації, коли ми хочемо проігнорувати подію і пропустити виконання автоматом будь-якої дії. Для моделювання такої поведінки ми вводимо новий тип дії.

struct Nothing
{
template <typename Machine>
void execute(Machine&)
{
}
};

Тепер нам просто потрібно підключити його до нашої поточної реалізації StateMachine.

template <typename Event>
void handle(const Event& event)
{
auto passEventToState = [this, &event] (auto statePtr) {
statePtr->handle(event).execute(*this);
};
std::visit(passEventToState, currentState);
}

Припустимо, що у нас є певний стан Foo, а ми хочемо перейти у стан Bar у випадку настання події Trigger і ніяк не реагувати на подію Ignored. У такому випадку реалізація стану може виглядати наступним чином:

struct Foo
{
TransitionTo<Bar> handle(const Trigger& event)
{
/* some important calculations etc. */
return {};
}
 
Nothing handle(const Ignored&)
{
return {};
}
};

Приклад

Погляньмо, як все це працює разом, реалізувавши простий автомат, який представляє собою двері. Існує два стани (Зачинено та Відчинено) та дві події (Відчинити, Зачинити). Схема переходів виглядатиме наступним чином:

12 1 - Реалізація автомата у C++17
#include <iostream>
#include <tuple>
#include <variant>
#include <functional>
 
template <typename... States>
class StateMachine
{
public:
template <typename State>
void transitionTo()
{
currentState = &std::get<State>(states);
}
 
template <typename Event>
void handle(const Event& event)
{
auto passEventToState = [this, &event] (auto statePtr) {
statePtr->handle(event).execute(*this);
};
std::visit(passEventToState, currentState);
}
 
private:
std::tuple<States...> states;
std::variant<States*...> currentState{ &std::get<0>(states) };
};
 
template <typename State>
struct TransitionTo
{
template <typename Machine>
void execute(Machine& machine)
{
machine.template transitionTo<State>();
}
};
 
struct Nothing
{
template <typename Machine>
void execute(Machine&)
{
}
};
 
struct OpenEvent
{
};
 
struct CloseEvent
{
};
 
struct ClosedState;
struct OpenState;
 
struct ClosedState
{
TransitionTo<OpenState> handle(const OpenEvent&) const
{
std::cout << "Opening the door..." << std::endl;
return {};
}
 
Nothing handle(const CloseEvent&) const
{
std::cout << "Cannot close. The door is already closed!" << std::endl;
return {};
}
};
 
struct OpenState
{
Nothing handle(const OpenEvent&) const
{
std::cout << "Cannot open. The door is already open!" << std::endl;
return {};
}
 
TransitionTo<ClosedState> handle(const CloseEvent&) const
{
std::cout << "Closing the door..." << std::endl;
return {};
}
};
 
using Door = StateMachine<ClosedState, OpenState>;
 
int main()
{
Door door;
 
door.handle(OpenEvent{});
door.handle(CloseEvent{});
 
door.handle(CloseEvent{});
door.handle(OpenEvent{});
 
return 0;
}

Вищенаведений код дає наступний результат:

Відчиняю двері...
Зачиняю двері...
Не зачиняються. Двері вже зачинені!
Відчиняю двері...

Підсумок

Звичайно, вищеописана реалізація далека від завершення. Є кілька питань, які потребують відповідей:

  • Що робити, якщо стани мають нетипову конструкцію і потребують передачі деяких аргументів під час ініціалізації?
  • Що робити, якщо під час виконання ми хочемо вирішити, до якого стану переходити, на основі інформації, присутньої у події?
  • Чи є спосіб створити візуальний опис автомата з самого коду?

3.9/5 ( голосів: 7)
Оцінка:
3.9/5 ( голосів: 7)

Вам також може сподобатися

Більше статей

Отримайте пропозицію

Якщо у вас виникли запитання або ви хочете дізнатися більше про наші пропозиції, зв’яжіться з нами.

Надіслати запит Надіслати запит

Tomasz Ukraine Business Lead

Get an offer

Працюйте в Sii

Знайдіть роботу, яка підходить саме вам. Перевірте відкриті вакансії та подайте заявку.

Подати заявку Працюйте в Sii

Viktoriya Recruitment Specialist

Join Sii

SUBMIT

This content is available only in one language version.
You will be redirected to home page.

Are you sure you want to leave this page?