単純なマーク付けがなされた文字列の解析 #11
二重のswitch文による状態遷移はコンパイラが吐くコードにもよると思うが十分速いものだと思う。
ただ、ソースの記述量が多く、長くなる傾向があり、コンパクトさに欠ける。
状態state
と入力c
による二重のswitch文は二次元のテーブルと等価となる。
そこで、二重switch文をテーブル参照に置き換えてみることを検討する。
このテーブルの項目の内容は、行うべきアクションと次に遷移すべき状態の組でいいだろう。
#include <stdio.h> #include <stdarg.h> void eprintf(char const *format, ...); void neprintf(char const *format, ...); void eprintf(char const *format, ...) { va_list ap; fputs("error: ", stderr); va_start(ap, format); vfprintf(stderr, format, ap); va_end(ap); fputc('\n', stderr); } void neprintf(char const *format, ...) { va_list ap; fputc('\n', stderr); va_start(ap, format); eprintf(format, ap); va_end(ap); } #define DEFINE_ACTION(name, code) \ static void act_##name(int c); \ static void act_##name(int c) {(void)(c);{code;}} #define P(name, next) {act_##name, next} DEFINE_ACTION(null, {}) DEFINE_ACTION(eprint_1, eprintf("illegal right parenthesis")) DEFINE_ACTION(neprint_1, neprintf("illegal right parenthesis")) DEFINE_ACTION(eprint_2, eprintf("unexpected EOF")) DEFINE_ACTION(neprint_2, neprintf("unexpected EOF")) DEFINE_ACTION(eprint_3, eprintf("illegal left parenthesis")) DEFINE_ACTION(neprint_3, neprintf("illegal left parenthesis")) DEFINE_ACTION(eprint_4, eprintf("null string between parentheses")) DEFINE_ACTION(start_string, printf("string [%c", c)) DEFINE_ACTION(end_string, puts("]")) DEFINE_ACTION(start_marked_string, printf("marked-string [%c", c)) DEFINE_ACTION(end_marked_string, puts("]")) DEFINE_ACTION(put_char, putchar(c)) enum tagState {START, RESTART, STRING, HEAD, BODY, ACCEPT, ERROR}; static struct tagTableItem { void (*action)(int c); enum tagState next_state; } table[][4] = { { P(null, HEAD), P(eprint_1, ERROR), P(eprint_2, ERROR), P(start_string, STRING) }, { P(null, HEAD), P(eprint_1, ERROR), P(null, ACCEPT), P(start_string, STRING) }, { P(end_string, HEAD), P(neprint_1, ERROR), P(end_string, ACCEPT), P(put_char, STRING) }, { P(eprint_3, ERROR), P(eprint_4, ERROR), P(eprint_2, ERROR), P(start_marked_string, BODY) }, { P(neprint_3, ERROR), P(end_marked_string, RESTART), P(neprint_2, ERROR), P(put_char, BODY) } }; static int code[] = { 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3,0,1,3,3,3,3,3,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 2 }; int main(void) { enum tagState state = START; do { int c = getchar(); struct tagTableItem *t = &table[state][code[c == EOF ? 256 : c]]; t->action(c); state = t->next_state; } while (state != ACCEPT && state != ERROR); return 0; }
少しだけ短くなった。行数でみるなら約6割に。
少しだけなのは、テーブルやアクションの定義が追加されていることによるもので、
switch文の塊だったmain関数は目論見通り劇的に短くできた。
一文字入力を受けるごとに、現在の状態state
と入力c
からテーブルの該当項目を参照して、
定められたアクションを実行し、次に遷移する状態をstate
に代入する。
これを状態がACCEPT
かERROR
になるまで繰り返すだけである。
テーブルtable
の入力c
項は本当は256文字+EOFの257項目あるので、
ACCEPT
とERROR
を除く5状態との組み合わせで、
テーブルサイズは5×257=1285項目必要だが、これはちょっと大きい。
そこで、左括弧、右括弧、EOF、その他の文字の4種類に分類し、
この4分類と5状態の組み合わせの20項目でtable
を構成している。
入力を4種類に分類するための表が配列code
である。
DEFINE_ACTION
マクロはstatic関数化したアクションをプロトタイプと定義に展開する。
速度的には二重のswitch文の方が速いだろうなあとは思う。
可読性についても向上しているかといわれるとどうだろう?返って読みにくいかも(ダメダメだ