続・ streambuf と iostream

http://d.hatena.ne.jp/janus_wel/20100616/1276702604 の続き。仕様に書いてある仮想関数で定義するべきことを要約する、というのが次にやるべきことなんだけどその前に streambuf の buf の部分、 buffering に関連することを理解する必要がある。とはいっても要は char 型もしくは wchar_t 型の配列 object ( これが buffer ) を経由して対象となる device とやりとりする、ということなんだけど。で、その際入力と出力ではそれぞれ、

  • 入力
    • 事前に対象の device とやりとりして buffer に data が入っている。
    • buffer の次の「位置」から char / wchar_t ひとつ分読み取って読み取る「位置」をひとつ分進める。
    • buffer を「全部読み尽くしたら」また対象 device とやりとりして data をもらう。
    • device 側がもう読ませる物はないよと言ってきたら「終了」。
  • 出力
    • buffer の次の「位置」に char / wchar_t ひとつ分書き込んで書き込む「位置」をひとつ分進める。
    • buffer を「全部書き尽くす」か「出力しろという命令」がきたら対象 device に出力する。

というようなことをやっているわけだ ( かなり大雑把にかつ状況を限定して言ってるのでもっと複雑なこともできる、はず ) 。でまぁ上記で「」で括っているところがキモなわけで、それぞれ以下のように読み替える。

  • 「位置」
    • 実体は buffer のどこかを指す pointer 。入力と出力でそれぞれ始点、次点、終点の 3 つずつある。
    • 入力側はそれぞれ eback() ( Entry BACK ? ) 、 gptr() ( Get PoinTeR ) 、 egptr() ( the End of Get PoinTeR ) で取得可能。
    • 出力側はそれぞれ pbase() ( Put BASE ? ) 、 pptr() ( Put PoinTeR ) 、 epptr() ( the End of Put PoinTeR ) で取得可能。
    • 始点と終点は一度 ( setg() / setp() で ) 設定したら再度設定するまで変わらない。
    • 始点は buffer 中のどこか。別に最後尾でもいいんだけど無理矢理ひとつの buffer で double buffering 的なこととかやらない限り buffer の一番最初の位置が一般的かも。
    • 次点は snextc() / sbumpc() / sgetn() や sputc() / sputn() などの public 関数を呼び出すと ( つまり std::(i|o|io)stream 側で読み進める / 書き進めるような操作をすると ) 読み込んだ分だけ移動する。
    • 終点は buffer の最後尾のひとつ次の位置を指す。つまり buffer の外。もちろん dereference して中身に access しようとするとでろでろ吐くんだけど stream の仕組みはそういうことはしないはずなので自分で書く際に気をつければいい。いやまぁぶっちゃけ STL の algorithm に指定する iterator の終点と同じような感覚なんだけど。
  • 「全部読み尽くしたら」
    • 入力側で始点 pointer からどんどん読み進めていった結果、次点 pointer が終点 pointer と同じ位置かそれよりもあとになった状況。
  • 「全部書き尽くす」
    • 同様に出力側の始点 pointer からどんどこ詰め込んでいった結果、次点 pointer が終点 pointer と同じ位置かそれよりもあとになった状況。
  • 「終了」
    • どんな device が対象でも traits_type::eof() が終了を表わすことになっている、らしい。 device が file の場合しか考えてなかったのか最初にそう決めてしまったから慣習でそうなっているのか。多分後者。

さて本題。大体以下のようになる ( はず ) 。

  • void imbue(const std::locale&);
    • locale に依存する処理は getloc() 経由でやればいいわけなのでここでは locale が変更された際にやらなきゃならないことを書けばいいはず。 imbue event に対する event handler のような感覚 ?
  • basic_streambuf* setbuf(char_type* s, std::streamsize n);
    • n 個の size を持つ char_type 型の配列 s を buffer として使ってもいいし使わなくてもいい。
    • つまり buffering なんてやってられるかぼけーと突っぱねてもいいし buffer は固定幅派なんですという主張を貫いてもいいわけだ。こういう場合何もせずに this を返せばいい。
    • 逆に指定された配列を buffer として使う場合ここで setg() と setp() を呼び出して入力 / 出力の 3 つの pointer を設定すればいいわけだ。
  • pos_type seekoff(off_type off, std::ios_base::seekdir way, std::ios_base::openmode which = std::ios_base::in | std::ios_base::out);
    • off は offset の略ぽい。で、どこからの offset かというと第 2 引数の way で指定される位置からの offset ということらしい。で、 way には std::ios_base::beg ( begin ) 、 std::ios_base::cur ( current ) 、 std::ios_base::end のいずれかがくることになっている。名前見ればわかるとは思うんだけどそれぞれ始端から、現在位置から、終端からの意味。
    • 第 1 引数 off が正の場合 beg と cur は終端方向への offset 、 end は始端方向への offset 。
    • 第 3 引数の which は入力か出力かということ。場合分けは往年の bit 演算で if (which & std::ios_base::in == std::ios_base::in) とかやればいい。
    • stream 側で tellg() / tellp() が呼ばれたときも最終的に処理するのはこいつなので定義時は要注意。
  • pos_type seekpos(pos_type sp, std::ios_base::openmode which = std::ios_base::in | std::ios_base::out);
    • pos は position かな ? 多分 device の始端からの位置を指定するものだと思う。
    • 一般的な device なら seekoff() をきちんと定義しているなら seekoff(off_type(sp), std::ios_base::beg, which) とかやっとけば問題ないはず。
    • seekoff() と違う動作をさせたい場合もあるとは思うんだけど思いつかないな…。
  • std::streamsize showmanyc();
    • 仕様にも書かれているけど s-how-many-care 、扱える個数はいくらか ? という名前。さんざん言われてるとは思うけど C++ 仕様や STL命名規則はホントにおかしい。わけがわからない。
    • 今の状況で device から読み込める文字数を返さなきゃいけないらしい。
    • もう読み込めないなら -1 、文字数がわからないなら 0 を返しとけばいいのかな ?
  • std::streamsize xsgetn(char_type* s, std::streamsize n);
    • device から直接 size が n の配列 s に文字をつっこむ。
    • default だと sbumpc() の連続呼び出しと同じようなことをする ( こういう場合は大体 sbumpc() の連続呼び出しという実装になっていることが多い ) と書かれているのでそれだと効率が悪いとかもっと効率よくやれる場合にここを適宜定義する、と。
    • 例えば traits_type::copy() で済む場合はそうする、とか。
  • int_type underflow();
    • buffer から文字を読み込まなきゃいけないのに読み込めない場合に呼ばれる。読み込めない場合ってのは具体的に次点 pointer が設定されていない場合と次点 pointer が終点 pointer 以上になったとき。
    • ここでやるべきことは始点、次点、終点 pointer すべてを適切に設定すること、と書かれているんだけど普通は ( 必要ならば device から buffer に data を読み込んで ) 次点 pointer を次に読み込むべき buffer 位置に設定する、ということをすればいいはず。
    • なんというか、仕様がかなりわかりにくく書かれてるんだけどこれはパッと思いつくような上記の処理だけではなくて device からの data が buffer size よりも小さい場合には終点 pointer を buffer size よりも小さく設定しなきゃいけないわけだし、 double buffering 的なことをやるんだったら 2 つの buffer を交互に指定してやらないといけないわけなので、まぁなんというか最終的に setg() / setp() を呼びさえすれば君らは何をやってもいいよということを言っているわけだ。
  • int_type uflow();
    • underflow() の動作 + 次点 pointer をひとつ進めるという動作をしないといけないらしい。
    • default で underflow() を呼び出したあと次点 pointer をひとつ進めるという動作みたいなので underflow() をしっかり定義してあればこっちをいじる必要はないぽい。
    • underflow() は重いんだけど uflow() の制約なら軽く済ませられる場合 ( 思いつかん… ) はこっちで全部やってしまうとか underflow() とは違う動作をさせるとかができるわけだ。
  • int_type pbackfail(int_type c = traits_type::eof());
    • 入力側の次点 pointer をひとつ戻す ( 読み進めたことをなかったことにする ) とか入力側に文字を書き戻すのに「失敗したとき」に呼ばれる関数。
    • 例えばちょうど buffer を読み尽くしたあとで underflow() が呼ばれてて以前読んだときの buffer の内容と異なっているような状況で次点 pointer を戻していった場合なんかを考慮すればいいのかな。
    • ということは device とのやりとりも含めてけっこうめんどくさい処理になりそう。
  • std::streamsize xsputn(const char_type* s, std::streamsize n);
    • size が n の配列 s から device に直接文字をつっこむ。
    • xsgetn() の出力版。
  • int_type overflow(int_type c = traits_type::eof());
    • buffer に文字を書き込まなきゃいけないんだけど書き込めない場合に呼ばれる。次点 pointer が設定されていない場合と次点 pointer が終点 pointer 以上になったときが該当する。
    • こっちでもやるべきことは underflow() と変わらない。始点、次点、終点 pointer を適切に設定すればおk。普通は ( 必要なら対象 device に buffer の中身を書き込んで ) 次点 pointer を始点 pointer に設定してやればいいはず。
  • int sync();
    • 制御対象の device と buffer の中身を同期する ( synchronize ) 。
    • 要は出力しちゃうわけだね。

とまぁここまで理解出来れば自分で定義もできるだろうということで。