カスタムバッファ
 
(このコンテンツはメールマガジンの 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

/////////////////////////////
	
 
CEasyStringBufの中身  驚いているかもしれません。ものすごく簡単でしょう? これは一番簡単な「カスタムストリームバッファクラス」の例です。
 まず、このクラスは 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 なメンバ関数は「派生クラスから親クラスを操作するためのメンバ関数」ということです。
 
透明
透明
■ C++ : protected ( #21 )
 クラスのメンバが「どこからアクセスできるか」を指定するアクセス指定子には、 public private protected の3つがあります。
  public は「どこからでもアクセスできる」よう指定します。 public メンバはクラス内のメンバ関数からも、クラスの外からも、アクセスできます。
  private は「クラスの中からのみアクセスできる」よう指定します。 private メンバは同じクラスのメンバ関数からのみアクセスできます。
  protected は「 private に加え、派生クラスからもアクセスできる」よう指定します。
 
 この protected は、 public や private に比べるとちょっと違う感じがあると思います。それは protected が「特定の目的」にしか使わないからです。それは「派生先から呼んで欲しいメンバ関数」として指定したい場合、という目的です。
 
class CWithProtected
{
protected:
    void Print( const char *p_pch ) const
    {
        printf( "%s\n", p_pch );
    }
};
	
 
 というクラスがあったとき、「このクラスは派生クラスを作ることを前提に設計されている」と判断できます。そうでなければ、メンバ関数を public か private にすればいいからです。わざわざ「派生先から呼ばれる」ようにしてあるのですから、派生クラスを作らなければ意味がないわけです。
 protected なメンバは派生先から呼び出せます。
 
class CWithProtectedSub
    : public CWithProtected
{
public:
    void PrintThisClass() const
    {
        Print( "CWithProtectedSub" );    // protected メンバを使用。
    }
};

void Use_CWithProtectedSub()
{
    CWithProtectedSub cWith;
//  cWith.Print( "cWith" );    // これはダメ。
    cWith.PrintThisClass();
}
// CWithProtectedSub
	
 
 protected は「気軽に使ってはいけない」という点に注意してください。 protected なメンバは、派生先で public メンバを通すことで「 public メンバ」にしてしまう(させられてしまう)ことがあります。
 自分で基底クラスも派生クラスも作る場合には、そういったことは起きないでしょう。ですが、自分は基底クラスのみを作って、派生クラスは他の人が作る、という場合、 protected メンバを「使いづらいから派生クラスでpublic メンバ関数を作って呼び出すようにしよう」と考えられてしまうかもしれません。その結果、基底クラスが思わぬ動作をするかもしれないのです。
  protected は public に非常に近い存在です。メンバはできる限り private にし、必要最低限のメンバだけを protected や public にしましょう。
 
 C++ を使う場合、この例のように「文法と意味」の両方を考える必要があります。 protected は文法的には private に近い存在ですが、意味的には「派生クラスで使って」という希望を持ち、そして public に近い存在となります。 C++ を理解するには、この両面を理解する必要があるでしょう。
透明
透明
 
 以上から言えることは、 std::streambuf のメンバ関数には
 
:派生クラスでオーバーライドする仮想メンバ関数
:派生クラスから呼ばれる、基本クラス操作用の 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++ : 文字型 ( #23 )
  C 言語ではあいまいだった「文字型」は、 C++ では char wchar_t にほぼ決定しました。 char は1バイト文字、 wchar_t は2バイト文字(いわゆる Unicode )を格納します。
 標準 C++ ライブラリはこのふたつを使い分けられるように、ちょっと複雑な構造をしています。 iostream の中を覗いてみたとき、 std::ostream が std::basic_ostream のテンプレート引数を明示したものだったことに気付いたでしょう。
 
 標準 C++ ライブラリでは、文字列を扱うクラスはすべてクラステンプレートとして作られています。これらのクラスはプレフィックスに basic_ が付きます。クラステンプレートは、第1引数が char か wchar_t を指定するためのもの、第2引数が「トレイツ」を取得するためのクラスを指定するためのものです。
 「トレイツ」は、文字属性を取得するためのクラスで、 std::char_traits などのクラステンプレートが用意されています。トレイツは特定の文字列型の「コピー」や「比較」「型キャスト」などを請け負います。
 
 複雑だと感じるかもしれません。実際、複雑です。
 C++ 標準化には、時間がかかりました。最初 iostream にはこれらの仕組みは存在しませんでした。実際、将来は std::stringstream に置き換えられる予定の std::strstream は、このような仕様変更がなされていません。古い iostream は、どれもが std::strstream のように char 型専用のクラスとして作られていました。
 それが、 Unicode への対応などにより、柔軟に処理できるようクラステンプレート化され、「トレイツ」などのシステムが加えられ、使いやすさを守るよう「古い形式」のクラスを typedef で作っておいたのです。
 
 標準 C++ を読み解く上で、この複雑なシステムは大きな壁となるでしょう。テンプレートを通して、各クラスが複雑に絡み合っています。読み進める時にはテンプレート引数を特定の型に置き換えて考えた方がいいかもしれません。
 
 このシステムは「テンプレート」を通して結合しています。そのため、非常に柔軟な設計になっています。将来、 wchar_t 以外の文字型を使う必要が出た場合でも、専用のトレイツクラスを作り、テンプレート引数に当てることでこれまで使用していた標準 C++ ライブラリをそのまま使えるでしょう。
 この「 STL & iostream 入門」を読み終えた後も、標準 C++ ライブラリを使いこなすためにはさらなる努力が必要でしょう。その壁は、このような「柔軟性を守るための複雑なシステム」によるものです。構造を理解し、使いこなせるようになれば、標準 C++ ライブラリは将来に渡って使い続けられる頼もしい味方となることでしょう。
透明
透明
 
(C)KAB-studio 2001 ALL RIGHTS RESERVED.