PPPUC++ #8.666666....

6.6 Trying the first version

  • さあ、準備はできたので main()をこさえて、get_token()をつけて、実行してみよう
  • 当然ですが、動作はおかしい。
    • まず数字単体をいれても動かん。どうなってんぞ。
    • さらに入れる。動かん。。。。もう一文字入れる。やっと帰ってくる。
    • 単純な足し算いれてみる。最初の値が帰ってくる。
    • わーい。 xをいれたら Bad Tokenって出たぞー、仕様通りだー。と、これくらいしか喜ぶ場所が無いです。

1
2
3
= 1
4
5
6
= 4
9+1
= 9
x
Bad token

    • まあここまで見れば3トークンパターン(x OP y) というふうになるパターンでだいたいわかりますよね。終端ミスですな。
    • ということでコードを見てみると、予想通り終端文字処理が無い。さて、どう直していくのでしょうwktk
      • ちなみに自分の練習用としては、term()にcin.putback()追加してリカバリ、'_'を終端として get_token()にそのまま'_'トークンを戻させるようにしてみましたが、括弧が戻せてなかったみたい。で、じゃあってことで expression()に cin.putback()いれたら、今度はパースエラー。そりゃそうか、ちゃんと')'がきたときだけ戻すようにしないと、ゴミが残るか。ってな感じで最終的には下の式は全てオK。デバッガなしってのは結構めんどうですね。本文進めます。

1+3_
= 4
2+2*3_
= 8
(2+2)*3_
')' expected

  • 一つ目のバグはやはりexpression()の TERM の次 ('+'|'-') がこなかった場合にトークンを「喰らって」いたから。というわけで putback()で取り上げるのが正解。
    • しかし、(僕が思わずやってしまったように) cinに返してはダメ。なぜなら扱っているのは キャラクタではなくトークンだから(今回の仕様では偶然問題ないですが・・・間違い、ちゃんと問題あります。一文字数字でしかテストしないというレガシーな事をしていたのがバカでした)
    • というわけでget_token()の代わりに Token Streamみたいなのから.get()することが必要

6.7 Trying the second version

  • という修正を行った版(まだ無い)を試してみるとまだバグがある。
  • 最後の結果が表示されない、というヤツ。
  • 本書では ';' で終端。そうだなあ、そっちの方が自然だよねしまった。
  • これで問題なく動くようになりました。
    • putbackしまくり。個人的には苦手なコード
  • とりあえずの初版としてはこんなもんでおk。ここから直していく
    • まず肝心な、Token Streamが未実装だったり

6.8 Token streams

  • 入力なしでは何も出来ない。これ先にやんなきゃ。なんだけど完成イメージを優先したそうな。
  • 仕様としては以下
    • cinからトークンごとに返してくれる
    • トークンを読み過ぎた場合に1個戻せるようにする
      • これが何故必要か、というと例えば、11.5+4 という式の 11.5の終端を知るためには '+'を先読みしないとだめだよね、ということ(このプログラムではこの終端検出自体はcinに隠蔽されているけれど、それ以外で必要)。
  • classメンバのpublic: private: の話。この区別が必要なのって、結局のところconvension、「ここまでは間違いない」という切り分けをするため、と思っておいていいのだよね。
    • 本文中では「コードを構造化する為の強力なツール」ということになっている
  • Token_streamで必要になる public I/Fは コンストラクタ、get(), putback()の3つ。
    • putback()などと、仰々しい名前にしたのは put()だと対称と勘違いされる危険がある(つまり get(), put()が対等と思われる)というのと、iostreamでそうなっているから。名前の一貫性重要。
6.8.1 Implementing Token_stream
  • どうかく?
    • コンストラクタは自明。
    • putback()で書き戻せるのは1トークンのみという制限。フラグで入っているかどうかを保持する。
    • get()は get_token()とほとんど一緒(と言っても本文中では今回が初出だけど)。上記フラグをチェックして書き戻されたものが無いかチェックする。あればそっちを返す。なければ cin で粛々と lexする。
  • ついでにクラス定義外部でのメンバ関数の書き方 (class_name) :: (member_name) の説明
    • なぜクラス定義外で書くのか? = 主に明確さのため。クラス定義はクラスが「何を出来るか」を記述する場で「どうやっているか」は分けるべきことが多い。
    • またクラス定義が長くなるとわかりにくいから(理想は一画面にすべて収まること)。
      • もう一つ、効率、があるとおもうんだけど、この時点では触れてないようです。
  • putback()の定義は preconditionとして fullフラグがfalse (=バッファが空)であることをチェック。先客がいたらエラー
6.8.2 Reading tokens
  • 最後に残ったget()が実際の作業をやります
    • fullフラグがtrue(先客あり)ならば、それを返してフラグをfalseに
    • fullフラグがfalse(先客がいない)ならば、cinから文字列読み込んでトークンを構築します
    • やり方は単純で、一文字読んで cin >> ch 、その値が '+'とかの演算子ならそのままそれでTokenを作成して返します
    • 数字だった場合、一文字数字をcin.putback()で戻してから cin >> dval; をやり直して Tokenを作成して返します
      • 素晴らしいことに指数表現でも最初は単に数字か'.'なので、あとはcinが良きにはからってくれます。0x, 0, 0bとかの接頭字はさすがにムリですが feature creep!
    • 想定外の場合は error("Bad token");
6.8.3 Reading numbers
  • 先走ってしまった。上の数字だった場合、の内容です。
  • 楽しまくってることは意味があるのです
    • 「良いプログラマは怠惰である :-P」  *すばらしい*

6.9 Program structure

  • 格言に「木を見て森を見ず」とあるけれど、たしかにfunction, class, などなど具象に目を囚われていると、プログラム全体を見失うことがあります。
  • というわけで単純化してみせるよ! という話
    • vim使いにとっては foldmethod=syntax状態ですね :D
#include "..."

class Token { /* ... */ };
class Token_stream {/* ... */};

Token_stream::Token_stream() .... { }
Toekn_stream::putback(Token t) { /* ... */ }
Token_stream::get() { /* ... */ }

Token_stream ts;
double expression();

double expression(); // forward declare

double primary() { /* ... */ }
double term() { /* ... */ }
double expression() { /* ... */ }

int main() { /* ... */ }
  • 順番重要. C++では宣言される前の名前は使えない
    • あれれ? オブジェクトの依存グラフを書いてみると

    • 循環あるよ>< なので何らかの方法で先に定義をしないと順番重要、の要件を満たせない。ので、ここでは expression()を前方参照(定義)にしています。
  • さて、これで完成? 
    • いやまだだ。
    • 経緯: 
      • 6.6 ファーストバージョン :全然ダメ。計算どころかそのまますらでない。バグputback忘れを修正
      • 6.7 セカンドバージョン : ちょっとマシ。でもやっぱり何かがおかしい。バグ終端忘れを修正して、なんとかOK
  • まだまだ。とりあえず基本の考え方を確かめるには至ったけれども、やることいっぱい。
  • というわけで次の章に続く。。。