scdmb scdmb - 4 months ago 23
C++ Question

Why a nested QStateMachine inside subclass of QState with this pointer in constructor causes that an outer state machine can't make a transition?

There is a state machine (called the outer). This machine has two states - first and the final. First state is custom implemented. Inside the first state there is created another state machine (called the inner) which in this example does nothing.

Outer state machine with two states:

#include <QDebug>
#include <QCoreApplication>
#include <QTimer>
#include <custom_outer_state.hpp>
#include <QFinalState>
#include "a.hpp"

class Functor
{
public:
void operator()()
{
// just emits a signal
TestObject outerTestObject;

// create outer state machine with all states
QStateMachine outerStateMachine;
CustomOuterState *state1 = new CustomOuterState();
QFinalState *state2 = new QFinalState();
state1->addTransition(&outerTestObject, SIGNAL(testObjectSignal()), state2);
outerStateMachine.addState(state1);
outerStateMachine.addState(state2);
outerStateMachine.setInitialState(state1);
outerStateMachine.start();

// process state machine transitions
QCoreApplication::processEvents();
qDebug() << &outerStateMachine << ": Outer state machine first state " << outerStateMachine.configuration();
outerTestObject.testObjectSignal();
QCoreApplication::processEvents();
qDebug() << &outerStateMachine << ": Outer state machine second state " << outerStateMachine.configuration();
}
};

int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QTimer::singleShot(0, Functor());
return QCoreApplication::exec();
}


And custom state:

#ifndef CUSTOM_OUTER_STATE_H
#define CUSTOM_OUTER_STATE_H

#include <QState>
#include <QStateMachine>

class CustomOuterState : public QState
{
Q_OBJECT
public:
virtual void onEntry(QEvent * event)
{
// create inner state machine
machine = new QStateMachine();
/*
* some operations with the machine
*/
}
private:
QStateMachine* machine;
};

#endif


And test object which just emits a signal:

#ifndef A_H
#define A_H

#include <QObject>

class TestObject : public QObject
{
Q_OBJECT
signals:
void testObjectSignal();
};

#endif


So this code works as expected - the outer state machine goes from the first state to the final:

QStateMachine(0x7fffc00f0a20) : Outer state machine first state QSet(CustomOuterState(0xe0a380) )
QStateMachine(0x7fffc00f0a20) : Outer state machine second state QSet(QFinalState(0xe0a460) )


But with a little change inside the custom state - passing
this
(which is a subclass of
QState
) to the inner state machine constructor

- machine = new QStateMachine();
+ machine = new QStateMachine(this);


results that the outer state machine doesn't want to make a transition - it stays in the first state although the transition signal was sent

QStateMachine(0x7fff219edcb0) : Outer state machine first state QSet(CustomOuterState(0x1fc4380) )
QStateMachine(0x7fff219edcb0) : Outer state machine second state QSet(CustomOuterState(0x1fc4380) )


The solution is simple - just delete the inner state machine and everything works. But the question is why the bad thing happens.

So, why adding
this
which is a subclass of
QState
to the inner state machine constructor results so that the outer state machine doesn't want to make a transition?

Answer

You're trying to modify a state machine's statechart from a state's event handler. That's not supported by the state machine. Since QStateMachine is-a QState, by adding it as a child you're modifying the statechart.

When onEntry is called, the statechart looks as follows:
statechart before
During onEntry, you change it to something resembling the following. The machine is not an initial state, it's just a dangling, useless state. Even if it weren't dangling, it'd still be unusable as it was added during a state transition.
statechart after

Since QStateMachine is a QState, when you make it a direct child of a state it becomes a substate of that state. If all you want is to use the state as a container for the state machine, you can insert an intervening non-state object between the parent and the state.

// https://github.com/KubaO/stackoverflown/tree/master/questions/statemachine-nested-32619103
#include <QtCore>

struct OuterState : QState
{
   QStateMachine * machine { nullptr };
   virtual void onEntry(QEvent *) Q_DECL_OVERRIDE
   {
      // through an intervening container
      auto container = new QObject(this);
      machine = new QStateMachine(container);
   }
   OuterState(QState * parent = 0) : QState(parent) {}
};

Finally, the pseudo-synchronous code that invokes processEvents is unnecessary. You can do things when states are entered, etc. Recall that since a signal is an invokable method just like a slot is, you can connect signals to signals. In fact, since all you want to achieve is an unconditional transition, you might as well use one.

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

   // create outer state machine with all states
   QStateMachine outerStateMachine;
   OuterState state1 { &outerStateMachine };
   QFinalState state2 { &outerStateMachine };
   state1.addTransition(&state2);
   outerStateMachine.setInitialState(&state1);
   outerStateMachine.start();

   a.connect(&state1, &QState::entered, []{ qDebug() << "state1 entered"; });
   a.connect(&state2, &QState::entered, []{ qDebug() << "state2 entered"; });
   a.connect(&state2, &QState::entered, qApp, &QCoreApplication::quit);
   return a.exec();
}

The above is, more or less, how self-contained test cases should look: single file, no fluff and verbosity that detracts from the issue at hand.

Comments