iostream の仕組み
 
(このコンテンツはメールマガジンの STL & iostream 入門に手を加えたものです。「 STL と iostream が使えるかのチェック」等はメールマガジンの方のページをご覧ください)
 
 iostream の仕組み ( #16 )
 
 STL がそうだったように、 iostream も非常に高い「カスタマイズ性」を持っています。「ないものは作ればいい」に強く応えてくれます。ですが、そのためにはまず iostream 全体の構造を知っておく必要があります。
 
 iostream のクラスは、だいたい3つのグループに分かれます。これは「ストリームクラス」「フォーマットクラス」「バッファクラス」です。
 これまで紹介してきた std::strstream などのクラスは、すべて最初の「ストリームクラス」に属するクラスです。普段使用する場合(つまりカスタマイズする必要がない場合)にはこのストリームクラスしかお世話にならないというわけです。
 このストリームクラスは、「フォーマットクラス」と「バッファクラス」を組み合わせたものです。つまり
 
ストリームクラス = フォーマットクラス + バッファクラス
 
 ということです。カスタマイズの第一歩は、これらを分解して考えることです。これを std::strstream と std::fstream のふたつのストリームクラスで見てみると、次のようになります。
 
Stream Class    = Format Class  + Buffer Class
――――――――――――――――――――――――
std::istrstream = std::istream  + std::strstreambuf
std::ostrstream = std::ostream  + std::strstreambuf
std::strstream  = std::iostream + std::strstreambuf
――――――――――――――――――――――――
std::ifstream   = std::istream  + std::filebuf
std::ofstream   = std::ostream  + std::filebuf
std::fstream    = std::iostream + std::filebuf
	
 
  std::istream std::ostream std::iostream は「フォーマットクラス」です。これらフォーマットクラスは、 << や >> 演算子を持っていて、文字列形式への整形を行います。
  std::istream >> を持ちます。 >> を使うと、まず「バッファクラス」から文字列形式として取り出し、それを指定された型へと変換して値を返します。
  std::ostream << を持ちます。 << を使うと、渡された値を文字列形式へと変換して、それを「バッファクラス」へと書き込みます。
  std::iostream このふたつを組み合わせたものです。
 
 重要なのは、フォーマットクラスは文字配列やファイルへと直接アクセスしないということです。フォーマットクラスは文字列形式への変換を行い、その文字列形式のデータを「バッファクラス」とやりとりするだけです。
 
 実際に文字配列やファイルへとアクセスするのは「バッファクラス」の方です。バッファクラスは文字配列やファイルなどと生のデータのやりとりを行い、それを文字列形式へと相互変換してフォーマットクラスとのやりとりに使用します。
  std::strstreambuf 文字配列とアクセスするクラスです。
  std::filebuf ファイルとアクセスするクラスです。
 
フォーマットとバッファ  つまり「データ形式の変換」と「操作対象へのアクセス」を別々のクラスに分けているのです。これが iostream の仕組みです。
 このおかげで、 std::strstream も std::fstream も同じように操作することができます。また、他の操作対象(例えばプリンタとか)に iostream を使用したい場合には、専用のバッファクラスを作って、フォーマットクラスと関連づけられているのです。
 「関連づけ」については、例を見た方がいいかもしれません。では、 std::strstream と同様の機能を、 std::iostream と std::strstreambuf を関連づけることで実現してみましょう。
 
////////////////////////////////////////////////////////////////
//  std::strstream のニセモノを作ります。
#include <stdio.h>
#include <iostream>
#include <strstream>

void MakeFake_strstream()
{
    char ch[130];
    std::strstreambuf cStrStrmBuf( ch, 128, ch );    // バッファクラス。
    std::iostream cIOStrm( &cStrStrmBuf );    // ストリームクラス。
    cIOStrm
        << "MakeFake_strstream()"
        << std::endl << std::ends;
    printf( "%s", ch );
}

// 結果
MakeFake_strstream()

/////////////////////////////
	
 
 どうでしょう、 std::strstream の時に似てる、と思いません? 違うのは、 std::strstream 型の代わりに std::strstreambuf と std::iostream の変数を作っているという点です。
 まず「バッファクラス」型の変数を作ります。入出力の相手は文字配列なので、文字配列用のバッファクラス std::strstreambuf を使います。
 このとき渡す引数は、第1引数と第2引数は std::strstream とほぼ同じです。第1引数には入力の相手になる文字配列へのポインタを、第2引数には文字配列のサイズを指定します。
 第3引数はちょっと違って、出力先のポインタを指定します。これは std::strstream とちょっと違いますね。実は第1引数は「読み取りポインタ」を、第3引数は「書き込みポインタ」を指定します。 std::strstream では読み取りポインタと書き込みポインタは独立していました。 std::strstreambuf を直接初期化するときには、別々に設定できるというわけです。ここでは std::strstream と同じように、両方とも同じポインタをセットします。
 
 バッファクラスが作れたらフォーマットクラスの std::iostream 型変数を作って、バッファクラスへのポインタを渡します。ここでは当然 std::strstreambuf 型変数へのポインタを渡します。このポインタを通して、フォーマットクラスはバッファクラスへとアクセスします。
 これで std::strstream のニセモノが完成しました。 std::iostream 型変数は std::strstream とほとんど同じように使うことができます。
 
 iostream の組み合わせ例 ( #17 )
 
 前回は「ストリームクラスはフォーマットクラスとバッファクラスの組み合わせ」ということを紹介しました。
 もうすこし正確に言うと「ストリームとして使用するためには、フォーマットクラスにバッファクラスへのポインタを渡す必要がある」ということになります。面白い例を見てみましょう。
 
////////////////////////////////////////////////////////////////
//  std::iostream の使用例です。
#include <stdio.h>
#include <iostream>
#include <fstream>
#include <strstream>

// バッファクラスを受け取ってフォーマットクラスに関連づけます。
bool UseFake_iostream( std::streambuf *p_pcStrmBuf )
{
    std::iostream cIOStrm( p_pcStrmBuf );
    cIOStrm
        << "UseFake_iostream()"
        << std::endl << std::ends;
    return true;
}

// 上の関数の使用例。
void MakeFake_steambuf()
{
    // std::strstream もどき。
    char ch[130];
    std::strstreambuf cStrStrmBuf( ch, 128, ch );
    UseFake_iostream( &cStrStrmBuf );
    printf( "%s", ch );

    // std::fstream もどき。
    std::filebuf cFBuf;
    cFBuf.open( "Data.txt", std::ios::out );
    UseFake_iostream( &cFBuf );
}

/////////////////////////////
	
 
 UseFake_iostream() は受け取ったバッファクラスにフォーマットクラスを結びつけて << 演算子を使って書き込む関数です。呼び出す側では、文字配列用の std::strstreambuf と、ファイル用の std::filebuf の両方を試しています。
 このようなことができるのは、フォーマットクラスが受け取るのが std::streambuf へのポインタだからです。 std::strstreambuf と std::filebuf は std::streambuf の派生クラスなので、どちらもフォーマットクラスと結びつけることができるのです。
 
透明
透明
■ C++ : ポリモーフィズム ( #16 )
 まずこのクラスを見てください。
 
class CBase
{
public:
    virtual void Print() const
    {
        printf( "CBase::Print()\n" );
    }
};
	
 
 メンバ関数 CBase::Print() に virtual が付いています。これが付いているメンバ関数を「仮想関数」といいます。
 あるメンバ関数に対して「派生クラスで新たに作り直してもいい」という許可を与えるのがこの virtual キーワードです。言い換えると、仮想関数は「派生クラスで新しく作ってもいいメンバ関数」だと判断できる、ということです。
 というわけで、ふたつほど派生クラスを作ってみましょう。
 
class CSub1
    : public CBase
{
public:
    virtual void Print() const
    {
        printf( "CSub1::Print()\n" );
    }
};

class CSub2
    : public CBase
{
public:
    virtual void Print() const
    {
        printf( "CSub2::Print()\n" );
    }
};
	
 
 CSub1 と CSub2 のふたつの派生クラスを作りました。どちらも Print() メンバ関数を持っています。このように「基底クラスの仮想関数と同じメンバ関数を作る」ことを「オーバーライドする」といいます。この例だと「 CSub1::Print() は CBase::Print() をオーバーライドした」と表現します。
 このようにオーバーライドしたときには、通常「基底クラスへのポインタか参照」を通して操作します。その例を見てみましょう。
 
////////////////////////////////////////////////////////////////
//    CBase 派生クラスの使用例。
#include <stdio.h>

void Use_CBasePointer( const CBase *p_cBase )
{
    p_cBase->Print();
}

void Use_CSubs()
{
    CSub1 cSub1;
    Use_CBasePointer( &cSub1 );
    CSub2 cSub2;
    Use_CBasePointer( &cSub2 );
}

// 結果
CSub1::Print()
CSub2::Print()

/////////////////////////////
	
 
 Use_CBasePointer() の中では CBase へのポインタを通して Print() メンバ関数を呼び出しています。一見 CBase::Print() が呼ばれそうですが、実際には CSub1::Print() か CSub2::Print() が呼ばれます。どちらが呼ばれるかは「実際に渡された変数」によって変わります。仮想関数は変数に「憶えてもらえる」ため、常に正しいメンバ関数が呼ばれるのです。
 この仕組みを「ポリモーフィズム」と言います。 Use_CBasePointer() はただの「 CBase 派生クラスの Print() メンバ関数を呼び出す」という機能だけを持ちます。実際にどのメンバ関数を呼び出すかは Use_CBasePointer()にどの CBase 派生クラスを渡すか、で決まるのです。
 今回の iostream で、フォーマットクラスに std::strstreambuf を渡しても std::filebuf を渡しても機能するのは、このふたつの基底クラスstd::streambuf クラスへのポインタを、フォーマットクラスが受け取って、仮想関数を呼び出すからなのです。
透明
透明
 
 この例からも分かるとおり、フォーマットクラスはバッファクラスから独立しています。対象への操作はバッファクラスに任せて、自分は << や >> を使った「インターフェイス」としての機能、そして文字列形式との適切な変換を行うという役に徹しているわけです。
 
ストリームクラス  ストリームクラスは便利に使えるよう、フォーマットクラスとバッファクラスをひとつのクラスに封じ込めています。正確に言うと「ストリームクラスはフォーマットクラスの継承クラスであり、バッファクラスをメンバ変数として持っている」のです。例えばこんな感じに。
 
////////////////////////////////////////////////////////////////
//  ニセ std::strstream クラスです。
#include <stdio.h>
#include <iostream>
#include <strstream>

// ストリームクラス
class CFake_strstream
    : public std::iostream  // フォーマットクラスから派生。
{
    std::strstreambuf m_cStrStrBuf;    //バッファクラス。
    typedef std::iostream type_Parent;    // VC の謎エラー対策。
public:
    CFake_strstream( char *p_pchStr, std::streamsize p_iSize )
        : m_cStrStrBuf( p_pchStr, p_iSize, p_pchStr )
        , type_Parent( &m_cStrStrBuf )
    {}
};

//    使用例。
void Use_CFake_strstream()
{
    char ch[130];
    CFake_strstream cStrStrm( ch, 128 );    // ニセ std::strstream 。
    cStrStrm
        << "Use_CFake_strstream()"
        << std::endl << std::ends;

    printf( "%s", ch );
}

// 出力結果。
Use_CFake_strstream()

/////////////////////////////
	
 
 ニセ std::strstream クラスの CFake_strstream は、 std::iostream から継承して、バッファクラス std::strstreambuf をメンバ変数として持っています。そしてコンストラクタの中で適切に初期化しています。たったこれだけでほとんど std::strstream と同様の機能を持つクラスになります。
 
透明
透明
■ C++ : 基底クラスの初期化 ( #17 )
 たとえばこんなクラスがあったとします。
 
class CBaseNoDefCon
{
private:
    CBaseNoDefCon()
    {}
public:
    CBaseNoDefCon( int p_i )
    {
        printf( "%d\n", p_i );
    }
};
	
 
 このクラスだと、普通に変数を作ることができません。
 
void Use_CBaseNoDefCon()
{
//    CBaseNoDefCon cBase;    // コンパイルエラー。
    CBaseNoDefCon cBase( 100 );    // これなら OK 。
}
	
 
 これは「引数のないコンストラクタ」が private だからです。なので、初期化には必ず int を引数にとるコンストラクタを呼び出す必要があります。
 これが問題になるのは、派生クラスを作った場合です。
 
class CSubNoInit
    : public CBaseNoDefCon
{};

// エラー例。
void Use_CSubNoInit()
{
//    CSubNoInit cSub;    // コンパイルエラー。
}
	
 
 CSubNoInit の基底クラス CBaseNoDefCon のコンストラクタは、必ず整数値を渡さなければいけません。ですが CSubNoInit はそうしてないので、エラーになってしまいます。
 派生クラスから基底クラスのコンストラクタを明示的に呼び出すには、 #13 で見た「メンバ変数を初期化する方法」と同じことをします。
 
class CSubWithBaseInit
    : public CBaseNoDefCon
{
public:
    CSubWithBaseInit()
        : CBaseNoDefCon( 100 )    // ここで基底クラスのを呼びます。
    {}
};

// 使用例。
void Use_CSubWithBaseInit()
{
    CSubWithBaseInit cSub;    // 今度は大丈夫。
}
	
 
 このように、メンバ変数の時と同じ方法で、基底クラスのコンストラクタを呼び出します。メンバ変数の初期化も、メンバ変数のコンストラクタを呼び出しているので、同じことと考えていいでしょう。
透明
透明
 
 ちなみになぜ「標準入出力」、つまり std::cout と std::cin について触れないかというと、「標準入出力」自体は OS に密接に関わっているため、ライブラリごとに実装方法が違うからです。あるライブラリは、専用のバッファクラス std::stdiobuf が用意されています。また他のライブラリは std::filebuf を使って普通のファイルとして標準入出力を操作します。このように異なるため、とりあえず解説は避けることにします。
 ただ std::cout が std::ostream の、 std::cin が std::istream の変数だということは確実なので、操作するときにはこれを踏まえていれば大丈夫でしょう。
(C)KAB-studio 2001 ALL RIGHTS RESERVED.