Заметки по языку с и с++

Реализация State Machine (конечного автомата) через QStateMachine

Введение

Многие части интерфейса и бизнес-логики приложения естественно описываются как конечные автоматы: состояние подключения (отключён/подключается/подключён/ошибка), состояние формы (редактирование/сохранение/сохранено), плеер (воспроизведение/пауза/остановлен). Реализация такой логики через разрозненные булевы флаги и условия быстро становится трудно поддерживаемой. Qt предоставляет встроенный модуль Qt State Machine с классом QStateMachine, который декларативно описывает состояния и переходы между ними. В статье построим конечный автомат для управления состоянием медиаплеера.

Концепция

QState представляет одно состояние автомата, QStateMachine управляет коллекцией состояний и текущим активным состоянием. Переходы между состояниями (QSignalTransition или QEventTransition) триггерятся сигналами объектов — например, переход из состояния «Воспроизведение» в «Пауза» срабатывает по сигналу pauseRequested(). Вход в состояние и выход из него можно связать с произвольными слотами через сигналы entered()/exited() каждого состояния, что позволяет выполнять побочные эффекты (включение/выключение элементов UI, запуск таймеров) автоматически при смене состояния, без ручной проверки условий в каждом месте кода.

Пример кода

#pragma once
#include <QStateMachine>
#include <QState>
#include <QObject>
#include <QDebug>

class MediaPlayerController : public QObject
{
    Q_OBJECT
public:
    explicit MediaPlayerController(QObject *parent = nullptr) : QObject(parent)
    {
        setupStateMachine();
    }

    void start() { m_machine.start(); }

signals:
    void playRequested();
    void pauseRequested();
    void stopRequested();

private:
    void setupStateMachine()
    {
        auto *stoppedState = new QState(&m_machine);
        auto *playingState = new QState(&m_machine);
        auto *pausedState = new QState(&m_machine);

        // Переходы из состояния "Остановлен"
        stoppedState->addTransition(this, &MediaPlayerController::playRequested, playingState);

        // Переходы из состояния "Воспроизведение"
        playingState->addTransition(this, &MediaPlayerController::pauseRequested, pausedState);
        playingState->addTransition(this, &MediaPlayerController::stopRequested, stoppedState);

        // Переходы из состояния "Пауза"
        pausedState->addTransition(this, &MediaPlayerController::playRequested, playingState);
        pausedState->addTransition(this, &MediaPlayerController::stopRequested, stoppedState);

        // Побочные эффекты при входе в каждое состояние
        connect(stoppedState, &QState::entered, []() { qDebug() << "Состояние: Остановлен"; });
        connect(playingState, &QState::entered, []() { qDebug() << "Состояние: Воспроизведение"; });
        connect(pausedState, &QState::entered, []() { qDebug() << "Состояние: Пауза"; });

        m_machine.setInitialState(stoppedState);
    }

    QStateMachine m_machine;
};
// Использование
#include "mediaplayercontroller.h"
#include <QCoreApplication>

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    MediaPlayerController player;
    player.start();

    emit player.playRequested();   // Остановлен -> Воспроизведение
    emit player.pauseRequested();  // Воспроизведение -> Пауза
    emit player.playRequested();   // Пауза -> Воспроизведение
    emit player.stopRequested();   // Воспроизведение -> Остановлен

    return app.exec();
}

Пояснения к коду

Три состояния (stoppedState, playingState, pausedState) создаются как дети m_machine, что автоматически передаёт владение памятью машине состояний. Переходы добавляются через addTransition(), привязывая конкретный сигнал к целевому состоянию — это явно ограничивает, какие переходы вообще возможны из текущего состояния: например, из stoppedState единственный валидный переход — по сигналу playRequested, и попытка испустить pauseRequested() в этом состоянии просто не будет иметь эффекта, поскольку такого перехода не существует.

Подключение к сигналу entered() каждого состояния демонстрирует, как выполнять побочные эффекты (в реальном приложении — изменение иконки кнопки play/pause, запуск/остановку таймера прогресса) автоматически и централизованно при входе в состояние, вместо разбросанных по коду проверок текущего состояния.

Подводные камни

  • Неявное игнорирование сигналов, не имеющих перехода из текущего состояния, без какой-либо ошибки или предупреждения — если ожидался переход, но он не сработал, это легко спутать с багом в логике, тогда как на самом деле переход просто не был объявлен для данного состояния.
  • Сложность отладки больших машин состояний с множеством переходов — визуально граф состояний и переходов не виден непосредственно в коде C++ (в отличие от, например, QML StateMachine с более декларативным синтаксисом) и требует либо отдельной документации/диаграммы, либо инструментов визуализации, поддерживающих экспорт состояния QStateMachine.
  • Использование QStateMachine для очень простых случаев (два-три состояния с однозначными переходами) может быть избыточным усложнением по сравнению с простым enum и switch-оператором — машина состояний оправдывает себя при заметном числе состояний и нетривиальных переходах с побочными эффектами.
  • Параллельные состояния (QState::ParallelStates) и история (QHistoryState) — более продвинутые возможности модуля, требующие отдельного изучения; неосторожное смешивание простых и параллельных состояний без понимания их взаимодействия может привести к неожиданному поведению при одновременной активности нескольких веток состояний.