Inheritance from a Statechart: refinement of the graphic


Inheriting HSM Models


Example: add % button to QCalc

  • One new transition triggered by % key code
  • Trigger sequence: a + b %
    Action triggered by %:
    result = a * (1 + b/100)
  • Trigger sequence: a - b %
    Action triggered by %:
    result = a * (1 - b/100)
  • Trigger sequence: a * b %
    Action triggered by %:
    result = a * (b/100)
  • Trigger sequence: a / b %
    Action triggered by %:
    result = a / (b/100)






  • Two steps in building derived model:
    1. Prepare base class
    2. Create derived class
    Derived QCalc

Step 1: Prepare base model

  1. Force base model to be a singleton
  2. Make base model handlers virtual
  3. No change in return statement for parent handler!

class Calc1 : public QHsm
{
public:
  static Calc1* instance();      // public singleton accessor method
protected:
  Calc1() : QHsm((QPseudoState)initial) {}  // protected constructor
protected: // state handlers
  void initial(QEvent const *e);
  virtual QSTATE calc(QEvent const *e);
    virtual QSTATE ready(QEvent const *e);
      virtual QSTATE result(QEvent const *e);
      virtual QSTATE begin(QEvent const *e);
    virtual QSTATE negated1(QEvent const *e);
    virtual QSTATE operand1(QEvent const *e);
      virtual QSTATE zero1(QEvent const *e);
      virtual QSTATE int1(QEvent const *e);
      virtual QSTATE frac1(QEvent const *e);
    virtual QSTATE opEntered(QEvent const *e);
    virtual QSTATE negated2(QEvent const *e);
    virtual QSTATE operand2(QEvent const *e);
      virtual QSTATE zero2(QEvent const *e);
      virtual QSTATE int2(QEvent const *e);
      virtual QSTATE frac2(QEvent const *e);
  virtual QSTATE final(QEvent const *e);
protected:  // other methods
  virtual void clear();
  virtual void insert(int keyId);
  virtual void negate();
  virtual void eval();
  void dispState(char const *s);
protected:  // data
  // windows stuff plus...
  double myOperand1;
  double myOperand2;
  int myOperator;
};

QSTATE Calc1::calc(QEvent const *e)
{
  switch (e->sig)
  {
    case Q_ENTRY_SIG:
      dispState("calc");
      return 0;
    case Q_INIT_SIG:
      clear();
      Q_INIT(&Calc1::ready);
      return 0;
    case IDC_C:
      clear();
      Q_TRAN(&Calc1::calc);
      return 0;
    case TERMINATE:
      Q_TRAN(&Calc1::final);
      return 0;
  }
  if (e->sig >= Q_USER_SIG)
  {
    isHandled = FALSE;
  }
  return (QSTATE)&Calc1::top; // unchanged 
}

QSTATE Calc1::zero2(QEvent const *e)
{
  switch (e->sig)
  {
    case Q_ENTRY_SIG:
      dispState("zero2");
      return 0;
    case IDC_1_9:
      insert(((CalcEvt *)e)->keyId); 
      Q_TRAN(&Calc1::int2);
      return 0;
    case IDC_POINT:
      insert(((CalcEvt *)e)->keyId);
      Q_TRAN(&Calc1::frac2);
      return 0;
  }
  return (QSTATE)&Calc1::operand2; // unchanged 
}

QSTATE Calc1::int2(QEvent const *e)
{
  switch (e->sig)
  {
    case Q_ENTRY_SIG:
      dispState("int2");
      return 0;
    case IDC_0:
   case IDC_1_9:
      insert(((CalcEvt *)e)->keyId); 
      return 0;
    case IDC_POINT:
      insert(((CalcEvt *)e)->keyId);
      Q_TRAN(&Calc1::frac2);
      return 0;
  }
  return (QSTATE)&Calc1::operand2; // unchanged 
}

QSTATE Calc1::frac2(QEvent const *e)
{
  switch (e->sig)
  {
    case Q_ENTRY_SIG:
      dispState("frac2");
      return 0;
    case IDC_0:
    case IDC_1_9:
      insert(((CalcEvt *)e)->keyId); 
      return 0;
  }
  return (QSTATE)&Calc1::operand2; // unchanged 
}
...
QSTATE Calc1::operand2(QEvent const *e)
{
  switch (e->sig)
  {
    case Q_ENTRY_SIG:
      dispState("operand2");
      return 0;
    case IDC_CE:
      clear();
      Q_TRAN(&Calc1::opEntered);
      return 0;
    case IDC_OPER:
      sscanf(myDisplay, "%lf", &myOperand2);
      eval();
      sscanf(myDisplay, "%lf", &myOperand1);
      myOperator = ((CalcEvt *)e)->keyId;
      Q_TRAN(&Calc1::opEntered);
      return 0;
    case IDC_EQUALS:
      sscanf(myDisplay, "%lf", &myOperand2);
      Q_TRAN(&Calc1::result);
      return 0;
  }
  return (QSTATE)&Calc1::calc; // unchanged 
}
...


Step 2: Create derived model

  1. Re-implement only handlers with changed behavior
  2. Cover only the new behavior
  3. Call base class handler for unchanged cases

class Calc2 : public Calc1
{
protected:
  virtual QSTATE operand2(QEvent const *e);
}; // that's all! only handlers (methods)  requiring modification

QSTATE Calc2::operand2(QEvent const *e)
{
  switch (e->sig)
  {
    case IDC_PERCENT:
      sscanf(myDisplay, "%lf", &myOperand2);
      switch (myOperator)
      {
        case IDC_PLUS:
          myOperand2 = 1.0 + myOperand2/100.0;       // x*(1 + y%)
          myOperator = IDC_MULT;
          break;
        case IDC_MINUS:
          myOperand2 = 1.0 - myOperand2/100.0;       // x*(1 - y%)
          myOperator = IDC_MULT;
          break;
        case IDC_MULT:                 // intentionally fall thru...
        case IDC_DIVIDE:
          myOperand2 /= 100.0;                       // x*y%, x/y%
          break;
        default:
          ASSERT(0);
      }
      Q_TRAN(&Calc2::result);
      return 0;                                  // event handled
  }
  return Calc1::operand2(e);    // let Calc1 handle other events
  /* NOTE: This returns the result of a function call to the base class
           handler, not directly a pointer-to-member as expected for a
           state handler.  The base class handler first handles the
           event, then returns the parent handler.  Thus the parent
           state is unchanged and determined by the base class
           handler. */
}


Polymorphism and Pointer-to-Member

The mechanism employed in this model inheritance is a polymorphic indirection not seen in typical OOP. The derived class method (handler) returns the address of a base class handler whiich in turn is called. This base class handler then calls another base class method which is overridden in the derived class. It is not entirely clear whether this call accesses the derived version of this overridden method.

Of course, you would expect that if things work as polymorphism policy requires, the call would differentiate at runtime, just as ordinary pointers to objects invoke the appropriate version at runtime. In fact this is accomplished. The optimal mechanism accomplishing this polymorphic indirection is the thunk, explained somewhat in the textbook and also in more detail in our course Generic Programming. The slide illustrates a simple demonstration program. The full program is quoted below, along with screen output. The code can be cut/paste into a file and tested on your own compiler.

Demo of polymorphic runtime binding applied to pointer-to-virtual-member

/* p2mdemo.cpp

     demonstration of polymorphic runtime binding
     applied to pointer-to-virtual-member functions
*/

#include <iostream>

class Base
{
public:
  typedef void (Base::*Handler)();

          Base();
  virtual ~Base();
  Handler f();
  virtual void g();
};

class Derived : public Base
{
public:
          Derived();
  virtual ~Derived();
  virtual void g();
};

              Base::    Base()     { std::cout << "Base::Constructor\n"; }
              Base::    ~Base()    { std::cout << "Base::Destructor\n"; }
              Derived:: Derived()  { std::cout << "Derived::Constructor\n"; }
              Derived:: ~Derived() { std::cout << "Derived::Destructor\n"; }
Base::Handler Base::    f()        { return &Base::g; }
void          Base::    g()        { std::cout << "  Base::g()\n"; }
void          Derived:: g()        { std::cout << "  Derived::g()\n"; }

Base B;
Derived D;

Base::Handler h = B.f();

int main()
{
  (B.*h)();
  (D.*h)();
}

/* Output:

Base::Constructor
Base::Constructor
Derived::Constructor
  Base::g()
  Derived::g()
Derived::Destructor
Base::Destructor
Base::Destructor

*/


Relaxing Singleton Assumption

Implications of multiple instances of base and derived machines


Multiple Inheritance


Four levels of complexity in deriving models