(このコンテンツはメールマガジンの STL & iostream 入門に手を加えたものです。「 STL と iostream が使えるかのチェック」等はメールマガジンの方のページをご覧ください)
|
文字列バッファクラスを作る ( #21 ) |
iostream が用意するバッファクラスは「文字列」「ファイル」「標準入出力」のみっつしか用意されていません(ライブラリによっては最後のはありません)。今回は、新たにバッファクラスを作ってみましょう。
と言っても、バッファクラスの拡張は一筋縄ではいきません。「文字列」と「ファイル」では内部的な操作が全然違います。その「内部」を自分で作るわけですから、それは大変なことです。 ということで、まずは簡単な例から試してみましょう。次のクラスは「ただ文字列を格納するだけのバッファクラス」です。 //////////////////////////////////////////////////////////////// // ただ文字列を格納するだけのバッファクラス。 #include <stdio.h> #include <iostream> class CEasyStringBuf : public std::streambuf { char m_chData[130]; public: // コンストラクタ。 CEasyStringBuf() { char *pchStart = m_chData; char *pchEnd = m_chData + 128; // 各ポインタをセットします。 setp( pchStart, pchEnd ); // 書き込みポインタ。 setg( pchStart, pchStart, pchEnd ); // 読み取りポインタ。 } }; // 使用例。 void Use_CEasyStringBuf() { CEasyStringBuf cBuf; std::iostream cIOStrm( &cBuf ); cIOStrm << 100 << " " << "two" << " " << std::endl << std::ends; char ch1[130], ch2[130]; cIOStrm >> ch1 >> ch2; printf( "%s, %s\n", ch1, ch2 ); } // 結果 100, two ///////////////////////////// 驚いているかもしれません。ものすごく簡単でしょう? これは一番簡単な「カスタムストリームバッファクラス」の例です。 まず、このクラスは std::streambuf からの派生クラスとして作られています。バッファクラスはすべてこのクラスから派生する必要があります。 このクラスは「文字列を格納する」ことが目的なので、文字配列をメンバ変数として持っておきます。この中に、渡された文字列を格納するわけです。 このクラスが持っている唯一のメンバ関数はコンストラクタです。 コンストラクタの中では、まずポインタふたつを用意します。これはメンバ変数として持っている文字配列の「先頭のポインタ」と「最後のポインタ」です。 次にこれを setp() という関数に渡します。これは std::streambuf が持っているメンバ関数です。このメンバ関数は「データを格納する変数へのポインタ」をセットします。 実は、バッファクラスは「文字配列」を強く意識しています。今回の例のように「文字配列」を内部に持って、そこにデータを書き込んでいくことを想定した設計になっています。 std::streambuf::setp() でセットされたポインタは、書き込み時に直接使用されます。 std::iostream などのフォーマットクラスは << で渡されたデータを「 char 1文字」に変換してからバッファクラスへと渡します。バッファクラスはこの文字列を「今あるポインタ」へと入れ、ポインタをひとつ進めます。これを繰り返すことで文字配列にデータが書き込まれていくのです。 このような「文字配列への操作」があらかじめ std::streambuf に備わっているおかげで、ポインタをセットするだけで自動的に読み書きが行われるというわけです。 話を戻しましょう。 std::streambuf::setp() は引数をふたつ取ります。第1引数は「書き込む場所の先頭ポインタ」を、第2引数は「同じく終端ポインタ」を渡します。 std::streambuf の自動書き込み機能は、このときセットした先頭ポインタから書き込み始めて、「終端ポインタ」で止まるようになっています。 もうひとつ、 std::streambuf::setg() というメンバ関数も呼び出します。これは「読み取り用ポインタ」をセットします。 std::strstream::seekp() と std::strstream::seekg() のように、書き込み用ポインタと読み取り用ポインタは独立していますから、同じくこの「ポインタの初期化」でも書き込みと読み取りの両方をセットしなければならないというわけです。 と言っても、ここではひとつの配列を使用するのでほとんど引数は同じです。このメンバ関数は引数をみっつ取りますが、第1引数と第3引数は std::streambuf::setp() と同じ、「読み取る場所の先頭ポインタ」と「同じく終端ポインタ」をセットします。第2引数は「読み取りポインタの最初の位置」です。第1引数と第3引数は「動ける範囲」を設定して、この第2引数は「最初の位置」を設定します。特別なことをしないのであれば、第1引数と同じでいいでしょう。 これで、 std::streambuf の内部ポインタをセットできました。あとは std::streambuf がその内部ポインタを使用して読み書きを行ってくれます。 |
カスタムバッファクラスでオーバーライド ( #22 ) |
ここで再び iostream の構造を思い出してみましょう。
ストリームクラスは、インターフェイスを持ち書式化を行うフォーマットクラスと、入出力の相手と直にやりとりするバッファクラスの組み合わせでなりたちます。 この「組み合わせ」について考えてみます。 std::iostream などのフォーマットクラスは、 std::streambuf 派生クラスへのポインタを受け取り、 std::streambuf の関数を呼び出すことでバッファクラスを操作します。このようなポリモーフィズムな操作を行うためには、メンバ関数が仮想関数である必要があります。 ここでちょっと std::streambuf の中を見てみてください。このクラスの中は、こんなふうになっていると思います。 // ライブラリによっては basic_streambuf の場合があります。 class streambuf { public: // pubseekpos() とか。 protected: void setp(char* p, char* ep); void setg(char* eb, char* g, char *eg); // その他色々。 virtual int sync(); // その他色々。 }; std::streambuf::sync() には virtual という単語が付いています。このように std::streambuf にはちゃんと仮想関数が用意されています。カスタムバッファクラスらしい処理をするためには、この仮想関数をオーバーライドすればいいということです。 また std::streambuf には非仮想で protected なメンバ関数もあります。前回使った setp() などがそうですね。 protected メンバ関数は「自クラスか派生クラスの中」からしか呼び出せません。わざわざ protected にしてあるということは「派生クラスの中から呼び出すため」のメンバ関数と考えるべきでしょう。つまり protected なメンバ関数は「派生クラスから親クラスを操作するためのメンバ関数」ということです。 |
|
以上から言えることは、 std::streambuf のメンバ関数には
1:派生クラスでオーバーライドする仮想メンバ関数 2:派生クラスから呼ばれる、基本クラス操作用の protected メンバ関数 の2種類があるというわけです。 前回は2の std::streambuf::setp() と std::streambuf::setg() しか使いませんでした。そこで、今度は1の、仮想関数のオーバーライドをしてみましょう。 //////////////////////////////////////////////////////////////// // 仮想関数のオーバーライドをしたバッファクラス。 #include <stdio.h> #include <iostream> class CRecycleStringBuf : public std::streambuf { char m_chData[130]; public: // コンストラクタは前回と同じ。 CRecycleStringBuf() { char *pchStart = m_chData; char *pchEnd = m_chData + 128; setp( pchStart, pchEnd ); setg( pchStart, pchStart, pchEnd ); } // 仮想関数。 virtual int sync() { *pptr() = '\0'; // 終端文字を追加します。 printf( "%s", m_chData ); pbump( pbase() - pptr() ); // 書き込み位置をリセットします。 return 0; } }; // 使用例。 void Use_CRecycleStringBuf() { CRecycleStringBuf cBuf; std::iostream cIOStrm( &cBuf ); cIOStrm << "One" << std::endl; //std::ends はいりません。 cIOStrm << "Two" << std::endl; } // 結果 One Two ///////////////////////////// この RecycleStringBuf クラスは、 std::streambuf::sync() をオーバーライドしています。このメンバ関数は「バッファリング」を行うときに自動的に呼ばれます。この使用例だと std::endl を渡したときに呼ばれます。 そこで、このクラスではこのメンバ関数の中で文字列を出力し、さらに「書き込み位置」をリセットする実装にしました。カスタムバッファクラスを作ればこういうこともできる、という例です。 sync() では pptr() pbump() pbase() という関数が使われています。これは std::streambuf の protected なメンバ関数、つまり派生クラスから呼び出す、基本クラスを操作するためのメンバ関数です。 これらのメンバ関数については、次回紹介しましょう。 |
操作用・オーバーライド用メンバ関数 ( #23 ) |
前回オーバーライドしたメンバ関数をもう一度見てみましょう。
virtual int sync() { *pptr() = '\0'; // 終端文字を追加します。 printf( "%s", m_chData ); pbump( pbase() - pptr() ); // 書き込み位置をリセットします。 return 0; } ここで使われている pptr() pbump() pbase() のみっつのメンバ関数は、std::streambuf の protected なメンバ関数です。このメンバ関数は、すでに std::streambuf に結びつけられたポインタを操作するためのものです。 ここで、 std::streambuf の protected なメンバ関数で、かつポインタ操作用のものをまとめて紹介しましょう。まずは書き込みポインタです。 put の p が付いています。 pbase() 書き込みポインタが移動できる範囲の先頭を示すポインタを返します。 std::streambuf::setp() の第1引数で指定したポインタです。 epptr() 書き込みポインタが移動できる範囲の終端を示すポインタを返します。 std::streambuf::setp() の第2引数で指定したポインタです。 pptr() 現在の書き込みポインタを返します。 最初は std::streambuf::setp() の第1引数です。 このポインタを利用して、書き込みを行います。 pbase() から epptr() の間でしか移動できません。 pbump() pptr() の位置を移動します。現在位置からの相対移動です。 マイナスで戻れます。 次は読み取りポインタ。 get の g が付いています。 gback() 読み取りポインタが移動できる範囲の先頭を示すポインタを返します。 std::streambuf::setg() の第1引数で指定したポインタです。 egptr() 読み取りポインタが移動できる範囲の終端を示すポインタを返します。 std::streambuf::setg() の第3引数で指定したポインタです。 gptr() 現在の書き込みポインタを返します。 最初は std::streambuf::setg() の第2引数です。 このポインタを利用して、読み取りを行います。 gback() から egptr() の間でしか移動できません。 gbump() gptr() の位置を移動します。現在位置からの相対移動です。 マイナスで戻れます。 関数名はともかくとして、仕組みとしては分かりやすいと思います。 で、実際の例として、再び std::streambuf::sync() について見てみましょう。 まず終端文字 '\0' を追加します。バッファリングが std::endl で行われてしまうので、 std::ends を渡せないため代わりにここで追加します。 pptr() を呼び出すと、現在の書き込みポインタの位置が返ってきます。この例なら 'e' や 'o' の次の要素、 std::RecycleStringBuf::m_chData[3] を指し示しているはずです。このポインタを通して直接 '\0' を追加します。 そうしたら出力。皆さんはここを色々変えるだけで、様々な機能が実現できると思います。 最後にリサイクルのため std::streambuf::pptr() の位置を先頭へと戻しておきます。 std::streambuf::pptr() を移動するのは std::streambuf::pbump() です。このとき渡す引数は「相対位置」なので、「先頭位置」を示す std::streambuf::pbase() から「現在位置」の std::streambuf::pptr() を引いたものを渡します。これで、 std::streambuf::pptr() の位置が std::streambuf::pbase() 、つまり std::RecycleStringBuf::m_chData に戻りました。これで再び先頭から書きむことができます。 戻り値は「失敗したときは−1」ということ以外は決まっていないので、とりあえずゼロを返しておきましょう。 ここまでで、カスタムバッファクラスを作るときのだいたいイメージは掴めたと思います。つまり簡単に言えば「仮想関数をオーバーライドして、その中で std::streambuf の protected メンバ関数を呼び出せばいい」というわけです。 ここで、オーバーライドしておくべき仮想関数を紹介します。 sync() フラッシュするときに呼び出されます。 領域をクリアにしたり、操作対象とアクセスしたりしましょう。 seekoff() ポインタを移動したいときに呼び出されます。 つまり std::ostream::seekp() を呼んだときに呼び出されます。 ポインタを移動させましょう。 これひとつで読み取り・書き込み両方操作する必要があります。 overflow() 書き込みポインタが最後まで来たとき呼ばれます。 エラーを返したり、領域を拡張したりしましょう。 underflow() 読み取りポインタが最後まで来たとき呼ばれます。 overflow() の読み取り版です。 「〜した時呼び出されます」というのは、ストリームクラスやフォーマットクラスに対してそういった操作が行われた時に呼び出される、ということです。例えば sync() であればバッファリングされたとき、つまり std::endl が渡されたときに呼ばれる、といった形です。 「〜しましょう」というのは、つまり皆さんがオーバーライドして、こういうふうに実装しましょう、ということです。もちろん必ずしもこうしなければならないというわけではありません。操作対象によって変わります。 実際には、もっと仮想関数が用意されているので、オーバーライドする関数は増えるかもしれません。逆に、これらの仮想関数にもデフォルトの機能が備わっているので、オーバーライドする必要がない場合もあります。その辺も、操作対象によって変わります。カスタムバッファクラスの製作が大変な面ですね。 |
ポインタ以外のカスタムバッファクラス ( #24 ) |
というより、カスタムバッファクラスを作る場合、操作対象はポインタじゃない場合がほとんどでしょう。その場合には、設計方法ががらっと変わります。でも「手法」、つまり「仮想関数をオーバーライドして protected メンバ関数を操作する」というのは変わりません。
変わるのは、必ずオーバーライドしなければならない仮想関数と、その実装方法です。 overflow() 文字を1文字書き込みます。 ポインタを1文字分進めます。 uflow() 文字を1文字読み取ります。 ポインタを1文字分進めます。 pbackfail() 文字を1文字戻します。 uflow() の呼び出しをキャンセルするためのものです。 underflow() 文字を1文字読み取ります。 ポインタは進めません。 普通は uflow() と pbackfail() の両方を呼び出します。 その実装例を見てみましょう。 //////////////////////////////////////////////////////////////// // 仮想関数のオーバーライドをしたバッファクラス。 #include <stdio.h> #include <iostream> class CNotPointerBuf : public std::streambuf { char m_chData[130]; char *m_pchData; public: // コンストラクタ。 CNotPointerBuf() : m_pchData( m_chData ) {} // 文字を1文字書き込みます。 virtual int overflow( int p_iChar = EOF ) { if( m_chData + 128 < m_pchData ) return EOF; *m_pchData = ( char )p_iChar; ++m_pchData; return true; } // 文字を1文字読み取ります。 virtual int uflow() { if( m_chData + 128 <= m_pchData ) return EOF; return (int)(unsigned char)*( m_pchData++ ); } // 取り出した文字を戻します。 virtual int pbackfail( int p_iChar = EOF ) { if( p_iChar == EOF ) return EOF; --m_pchData; if( *m_pchData != ( char )p_iChar ) return EOF; return p_iChar; } // 文字を1文字読み取りますが、移動しません。 virtual int underflow() { return pbackfail( uflow() ); } // バッファリングします。 virtual int sync() { *m_pchData = '\0'; printf( "%s", m_chData ); m_pchData = m_chData; return 0; } }; // 使用例。 void Use_CNotPointerBuf() { CNotPointerBuf cBuf; std::iostream cIOStrm( &cBuf ); cIOStrm << 100 << std::endl; char ch[130]; cIOStrm >> ch; printf( "%s", ch ); } // 結果 100 100 ///////////////////////////// まず、コンストラクタで std::streambuf::setp() などを呼び出していない点に注意してください。ポインタの初期化をしないことで、前回紹介した std::streambuf::pptr() などのポインタ操作関数がまったく機能しなくなります。 代わりに機能するのが前述の4つの仮想関数です。 書き込み時には overflow() が呼ばれるので、1文字ずつ返していくよう実装します。 読み取り時はちょっと複雑です。「読み取れるかどうか」をチェックしながら読み取る必要があるため「ポインタを進めず読み取る」操作と「ポインタをひとつ戻す」操作が必要になります。これと、普通に「1文字を返してポインタを進める」操作、この3つを実装します。 std::streambuf は、このように「文字列操作」としての機能と「それ以外への操作」としての機能の両方を持っています。必要に合わせて、実装方法を変えることになるでしょう。 |
|
|
(C)KAB-studio 2001 ALL RIGHTS RESERVED. |