Cの関数ポインタテーブルと状態遷移表
組み込みではよく設計フェーズで状態遷移表を作って、それを元に実装したり、試験したりする。状態遷移表はC言語の関数ポインタのテーブルと相性が良く、表をそのまま二次元配列に格納すると設計をそのまま実装として表す事が出来る。
関数ポインタのテーブルを使用するとif文・switch文を使わずに状態遷移表が書けるので、場合によっては可読性が向上するし、インデックスでテーブルの各行の関数にアクセスできれば実行速度的にも有利だ。
ただ関数ポインタを用いたテーブルは、入門的なC言語の書き方と異なるので、読むにも書くにも頭の整理が必要になる。頭の整理を兼ねて、状態遷移表をそのままC言語で表す例を書いてみることにした。ここでは、ifもswitchも(条件分岐として使用できるforもwhileも)使用しない縛りの中で、状態遷移の条件分岐を書いてみることにした。
状態遷移の仕様を仮定する
状態遷移表は、状態とイベントの2次元のマトリックスで表現する。例として、状態には腹の減り具合を、イベントには母親から「何々を食え・飲め」と言われたり、言われた通りに完食したりといった事を想定する。
ここで、以下の仕様と仮定する。
- 空腹であればラーメンも食えるしタピオカも飲めるが、胃薬は飲めない
- 食事中であれば食事完了まで何も飲み食い出来ない。
- 食事完了したら満腹になる。
- 満腹になったらラーメンは食えないが、デザートのタピオカは飲める。
- 満腹時に胃薬を飲むと空腹に戻る。
これを状態遷移表で表すと以下のようになる。
状態\イベント | ラーメンを食え | タピオカを飲め | 胃薬を飲め | 食事完了 |
空腹 | 食事中へ | 食事中へ | 拒否 | – |
食事中 | 拒否 | 拒否 | 拒否 | 満腹へ |
満腹 | 拒否 | 食事中へ | 空腹へ | – |
本当に満腹の時にタピオカが飲めるのか、そもそもタピオカはデザートなのか、胃薬を飲んだら空腹になるのかというツッコミは絶えないと思うが、仕様は絶対なのでそのとおりに実装することにする(おいっ)
また言うまでも無く、この仕様にはタピオカは空腹を挟まず無限に飲めるという明らかなバグが存在するが、やはり仕様は絶対なので気にせず、仕様通りに実装することにする(おいっ)
state_machine_sample.c
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 #include <stdio.h>// 状態一覧typedef enum {ST_KUFUKU, //空腹ST_SYOKUJICHU, //食事中ST_MANPUKU, //満腹ST_NUM, //状態の数} MY_STATE_T ;// イベント一覧typedef enum {EVENT_RAMEN, //ラーメン食えEVENT_TAPIOCA, //タピオカ飲めEVENT_IGUSURI, //胃薬飲めEVENT_SYOKUJI_COMP, //食事完了EVENT_NUM, //コマンドの数} EVENT_T;//イベントの関数型宣言typedef void ( *SENNI_FUNC )( EVENT_T event);//状態遷移表の1列分typedef struct {SENNI_FUNC senni_func[EVENT_NUM];} STATE_ROW_T;//プロトタイプ宣言static void GoToSyokujichu( EVENT_T event );static void GoToManpuku( EVENT_T event );static void GoToKufuku( EVENT_T event );static void Refuse( EVENT_T event );static void DoNothing( EVENT_T event );static void PrintEvent( EVENT_T event);static void PrintState( void );static void SetState(MY_STATE_T st);/*******************************************************************//************************** 各種変数定義 ****************************//*******************************************************************/static MY_STATE_T CurrentState = ST_KUFUKU; //現在ステータス(空腹で初期化)//表示用のイベント名const char *EventName[] = {"ラーメン食え","タピオカ飲め","胃薬飲め","食事完了",};//表示用のステータス名const char *StateName[] = {"空腹","食事中","満腹",};//状態遷移テーブル ★肝になる所const STATE_ROW_T StateCmdTable[ST_NUM] = {/* ラーメン タピオカ 胃薬 食事完了 *//* 空腹 */ {GoToSyokujichu,GoToManpuku, Refuse, DoNothing } ,/* 食事中 */ {Refuse, Refuse, Refuse, GoToManpuku} ,/* 満腹 */ {Refuse, GoToSyokujichu, GoToKufuku, DoNothing }};//食事中への遷移処理static void GoToSyokujichu( EVENT_T event ){PrintEvent(event);puts("食事中へ遷移");SetState(ST_SYOKUJICHU);PrintState();}//満腹への遷移処理static void GoToManpuku( EVENT_T event ){PrintEvent(event);puts("満腹へ遷移");SetState(ST_MANPUKU);PrintState();}//空腹への遷移処理static void GoToKufuku( EVENT_T event ){PrintEvent(event);puts("空腹へ遷移");SetState(ST_KUFUKU);PrintState();}//拒否の処理static void Refuse( EVENT_T event ){PrintEvent(event);puts("嫌だ!断固拒否!");PrintState();}//異常時に何もしない為の処理static void DoNothing( EVENT_T event ){PrintEvent(event);puts("何かがおかしい。何もしない");PrintState();}//現在状態の表示static void PrintState( void ){printf("現在のステータス: %s\n\n", StateName[CurrentState]);}//イベント名の表示static void PrintEvent( EVENT_T event ){printf("イベント: %s\n", EventName[event]);}//現在状態の設定static void SetState( MY_STATE_T st ){CurrentState = st;}//メイン処理int main(int argc, char *argv[]){EVENT_T event;puts("スタート");PrintState(); //初期ステータスの表示//胃薬飲めevent = EVENT_IGUSURI;StateCmdTable[CurrentState].senni_func[event](event);//ラーメンを食えevent = EVENT_RAMEN;StateCmdTable[CurrentState].senni_func[event](event);//食事完了event = EVENT_SYOKUJI_COMP;StateCmdTable[CurrentState].senni_func[event](event);//ラーメンを食えevent = EVENT_RAMEN;StateCmdTable[CurrentState].senni_func[event](event);//タピオカを食えevent = EVENT_TAPIOCA;StateCmdTable[CurrentState].senni_func[event](event);//食事完了event = EVENT_SYOKUJI_COMP;StateCmdTable[CurrentState].senni_func[event](event);//胃薬を飲めevent = EVENT_IGUSURI;StateCmdTable[CurrentState].senni_func[event](event);//食事完了event = EVENT_SYOKUJI_COMP;StateCmdTable[CurrentState].senni_func[event](event);return 0;}
実行結果は以下
123456789101112131415161718192021222324252627282930313233343536 [user@host-pc tmp]$ gcc -o state_machine_sample state_machine_sample.c[user@host-pc tmp]$ ./state_machine_sampleスタート現在のステータス: 空腹イベント: 胃薬飲め嫌だ!断固拒否!現在のステータス: 空腹イベント: ラーメン食え食事中へ遷移現在のステータス: 食事中イベント: 食事完了満腹へ遷移現在のステータス: 満腹イベント: ラーメン食え嫌だ!断固拒否!現在のステータス: 満腹イベント: タピオカ飲め食事中へ遷移現在のステータス: 食事中イベント: 食事完了満腹へ遷移現在のステータス: 満腹イベント: 胃薬飲め空腹へ遷移現在のステータス: 空腹イベント: 食事完了何かがおかしい。何もしない現在のステータス: 空腹
コメント