Last update 1999/09/22

iostreamの拡張

(C)平山直之
無断転載は禁止、リンクはフリー
誤字脱字の指摘は歓迎


iostreamの拡張について

例によって唐突ですが、iostreamの拡張について書いてみたいと思います。ここでいう「拡張」とは、自分でcinとかcoutみたいなものを作ろう、ということです。

「こんなもん一番最初に教えんなや」と比較的評判の悪いiostreamですが、

などのように表面的に美しくないのが悪評の原因で、その辺に目をつぶれば実はそれなりによくできたシステムだったりします。

拡張方法も一度覚えると結構便利ですぜ。

iostream拡張の基礎知識

iostream、というと、そのインターフェイス(Java的な意味じゃなくて)であるところのstd::cinstd::coutなどを思い浮かべるのが普通ではないかと思います。

ここでiostreamを拡張してcprinterなんてものを作りたくなったとしますよね。その場合、std::cinstd::cout)かその基底クラスあたりを派生して、何か適当な仮想関数をオーバーライドすると考えるのが普通ではないかと思います。

でも、実は違うんですね。といっても、iostreamがそういう「通常の予測」を裏切るのは、非直感的な悪い設計になっているからではなくて、普通の人が考えるよりもスマートな設計になっているからなんです。

では実際にどのようになっているかというと、

という決まりになっているのです。cin/coutやらstringstreamなんかは、実はbasic_streambufの派生クラスのプレースホルダに過ぎなかったのです。

というわけで、iostreamを拡張するということは、すなわちbasic_streambufを継承して実装するのとほぼ同じこと、ということになります。

非バッファリング編

さて実際にiostreamを拡張してみましょう。バッファリングを行わないもののほうが簡単なので、まずはそちらからチャレンジしてみましょう。

VC++でwin32のコンソールアプリケーションを作れることは、みなさんもうご存知ですよね。まあ知らなくても構わないのですが、とにかく作れるのです。

でもって「コンソールアプリケーション」では、C言語の入門書なんかに書いてあるように

#include <stdio.h>

void main(int,char**)
{
	printf("hello world\n");
	return 0;
}

とでも書けばDOSプロンプトみたいなウィンドウが出てきて「hello world」なんて表示されたりするわけです。

UNIXとかDOS上がりのCプログラマから見れば当たり前のことなんですが、生っ粋の(APIだけでアプリケーションを作れる)Windowsプログラマから見るとこれはちょっと変に見えます。どこが変かといえば、

なんてカンジです。

これらの鍵はもちろんVC++で「コンソールアプリケーション」を選んだことにあります。つまり、リンクのときにDOSプログラムっぽく動くようにしてくれたってわけですね。

さて、これを見て、フツーのWindowsアプリケーションを作っているWindowsプログラマは思うわけです。「俺にも使わせろ〜」と。GUIを用いたデータの出力は意外に面倒なものだからです(キャラクタ端末なら垂れ流してればかってにスクロールアウトしてくれるのにねえ)。また、キャラクタ端末はあからさまにデバッグプリントに便利そうです。

実は、Win32にはコンソール端末APIが用意されていて、それを使えば通常のWinMainから始まるWindowsアプリケーションでも、コンソールを使うことができるのです。バイブルにも載っていないっぽいですが。

というわけで、WinMainから始まるWindowsアプリケーションでも簡単にコンソールを使うことができるように、std::coutを拡張してみましょう。

ステップ1: streambufの拡張

最初に述べたように、iostreamの拡張=事実上streambufの拡張です。というわけで、streambufの拡張から始めましょう。ソースはこんな感じになります。

template <class Ch,class Tr=std::char_traits<Ch> >
class basic_win32console_streambuf : public std::basic_streambuf<Ch,Tr> {
public:
    basic_win32console_streambuf(void)
    {
        setbuf(0,0);
        AllocConsole();
    }
    ~basic_win32console_streambuf(void)
    {
        FreeConsole();
    }

protected:
    std::streampos seekoff( 
        std::streamoff off, 
        std::ios::seek_dir dir, 
        int nMode = std::ios::in | std::ios::out )
    {
        return EOF;
    }
    std::streampos seekpos( 
        std::streampos pos, 
        int nMode = std::ios::in | std::ios::out )
    {
        return EOF;
    }

    int overflow( int nCh = EOF )
    {
	char buffer[2];
	buffer[0]=nCh;
        DWORD size;
	WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE),buffer,1,&size,NULL);
        return 0;
    }

    int underflow(void)
    {
        return EOF;
    }

private:

};

最新のC++では、iostreamに含まれる各クラスは、その要素をテンプレート型とするテンプレートクラスとなっています。上のクラス定義で言うと、Chが要素の型になりますね。Trの方は当面気にする必要はありません。

で、せっかくですから、われわれが作るbasic_win32console_streambufも汎用的にしよう、ということで、テンプレートクラスになっています。当然のことながら、テンプレート型はそのまま親に渡します。

コンストラクタ、デストラクタ

コンストラクタとデストラクタは、当然のことながらこのオブジェクトが生成されたときと破棄されるときに呼ばれます。これらは通常そのプレースホルダとなるiostreamクラスの生成・破棄と同じタイミングになりますので、要するにファイルのオープン・クローズに相当する処理を実行すればよろしい。

ということで、コンストラクタではAllocConsole()を、デストラクタではFreeConsole()を実行します。それぞれコンソールウィンドウを開いたり閉じたりするWin32APIです。

それと、バッファリングはしないので、setbuf(0,0);としておきます。

seekoffseekpos

これらの仮想関数に関しては、そのストリームバッファがシークをサポートしない場合EOFを返しておけば問題ありません。正確には多分Tr::eof()を返すほうがベターなんだと思いますが、traitsの概念を明確には理解してないのでわかりません(笑)。traitsはどうも、その型の補助的な情報の集合を表したクラスらしいです。

overflow

このメンバ関数のオーバーライドがキモになります。通常streambufはバッファリングも司るので、本来ならこのメンバ関数はバッファがあふれたときだけ呼ばれるものです。しかしながらこのクラスではバッファを提供していないので、毎回バッファがあふれます。つまり早い話、ユーザがストリームに1文字挿入するごとにこのメンバ関数が呼ばれます。ということでこの場合、馬鹿みたいにそれをWin32APIに渡せばよろしい。それが上のコードです。

underflow

これはoverflowとは逆に、このストリームバッファを読み込みバッファとして使うときに、バッファに読み込む文字がなくなったときに呼ばれる関数です。今回読み込みは実装しないので無視します。

streambufの拡張のほうはこんなところです。

ステップ2: ostreamの拡張

こちらもstreambufと同じ理由で、クラステンプレートになっています。

template <class Ch,class Tr=std::char_traits<Ch> >
class basic_win32console_stream : public std::basic_iostream<Ch,Tr> {
public:
    basic_win32console_stream(void) 
        : std::basic_iostream<Ch,Tr>(new basic_win32console_streambuf<Ch,Tr>())
    {
    }

    ~basic_win32console_stream(void)
    {
    }

private:

};

先ほども延べたように、こちらは単なるプレースホルダであるのが明白ですね。streambufへのポインタは親クラスが持っているので大丈夫。このクラスではコンストラクタでstreambufを生成してそれを親に渡すだけです。

ステップ3: typedef

stdでも各種クラスのchar型としてのtypedefは

のように行われていますから、ここでもやっておきましょう。

typedef basic_win32console_streambuf<char> win32console_streambuf;
typedef basic_win32console_stream<char> win32console_stream;

ステップ4: 利用

これでもうOKです。Windowsアプリケーションからコンソールに出力してみたくなったら、

win32console_stream wcon;
wcon << "hello world";

とこんなカンジでよろしい。

ただし、このままだとwin32console_streamのインスタンスを複数生成したときなどにまずいっぽいので、気になる人はSingletonデザインパターンの適用などを検討してみましょう。

バッファリング編その1・zlibを使ってみる

zlibとは

圧縮ってのもまたカッタリィ世界でして、アルゴリズムで特許をとってライセンス料をとろうなんて輩が大勢いる(らしい)ので、うっかりするとすぐに特許に抵触してしまいます。

そこで現れたわれわれの救世主がzlibです。Cマガジン1998年10月号60ページの奥村晴彦氏の記事によると「商品への組み込みに際して作者の許可やソースコードの添付は不要」だそうですし、公式ホームページ(http://www.cdrom.com/pub/infozip/zlib/)でも

zlib is designed to be a free, general-purpose, legally unencumbered -- that is, not covered by any patents -- lossless data-compression library for use on virtually any computer hardware and operating system.

だそうですんで、とりあえずウノミにするともう最高にクールな圧縮ライブラリ、みたいなみたいなぁ。

おっとそうそう、使う前にはライブラリを用意しなければなりませんね。この辺にVC++でzlibを使う方法、みたいのが書いてあるので、参考にしましょう。

zlibとストリーム

さてこのzlibですが、とても使いやすくできています。何が使いやすいか、といえば、それはもう何を置いても「圧縮と伸長をメモリ上でできる」ことです。

ただし、使いやすいと言っても、それは「余計な仮定をしていないのでさまざまなケースに対応しやすい」という意味であって、「使うのがめちゃめちゃ簡単」という意味ではありません。zlibの作者が決めた決まりごとにはたくさん従わなければならないのです。そこで当然、その部分を補うべく、ラッパークラスを設計しようという発想が出てきます。

「フィルタ」という概念

この文脈で出てくるわけですから、言うまでもなくラッパークラスはiostreamの継承物にしようと企んでいるわけです。そうすれば、「zlibの作者の決めた決まりごと」の代わりに、われわれが慣れ親しんでいる「iostreamの決まりごと」に従えばプログラムを組めるわけですからね。

とは言っても、その実装が問題になってきます。zlibがわざわざ余計な仮定をしていないでいてくれているのに、ラッパークラスでたとえばそれを「ファイル入出力に限る」などと封殺してしまっては、使い道がとても限定されてしまいます。そんなラッパークラスではいくらなんでも使い物になりません。

そこで、「フィルタ」という概念を考案してみました。「cat t.txt | more」みたいなカンジです。こんな風に使うことを想定しています。

    // 圧縮
    {
        std::ifstream ifs("test.txt",std::ios::binary);

        std::ofstream ofs("test.out",std::ios::binary|std::ios::trunc);
        vafxzlib::ozfilter ozf(ofs);

        int c;
        while((c=ifs.get())!=EOF){
            ozf << (char)c;
        }
    }

    // 伸長
    {
        std::ifstream ifs("test.out",std::ios::binary);
        vafxzlib::izfilter izf(ifs);

        for(;;){
            std::vector<char> v(128);
            izf.read(v.begin(),v.size());
            if(izf.gcount()==0){
                break;
            }
            std::cout << std::string(v.begin(),izf.gcount());
        }
    }

水色の部分がそれで、コンストラクタに他のストリームへの参照を渡すと、以後フィルタへの出力は、加工された後にコンストラクタで渡された他のストリームへ送られることになるのです(入力は当然その逆)。

当然のことながら、コンストラクト・デストラクトの順序には理論上敏感なはずですが、C++の規約上、a・b・cの順で生成された局所変数はc・b・aの順で破棄されますので、どう考えてもヘンな使い方をしない限りそれが問題になることはないと思います。

basic_zstreambuf

ということでまずはbasic_streambufの拡張から。名前をbasic_zstreambufとして、入出力兼用とします。

template <class Ch,class Tr=std::char_traits<Ch> >
class basic_zfilterbuf : public std::basic_streambuf<Ch,Tr> {
public:
    typedef std::basic_streambuf<Ch,Tr> superclass;

public:
    // 出力コンストラクタ
    basic_zfilterbuf(std::basic_ostream<Ch,Tr>& os)
    {
        out=&os;
        in=NULL;

        sbuffer.resize(4096);
        dbuffer.resize(sbuffer.size());
        buffer  =sbuffer.begin();
        size    =sbuffer.size();
        setp(sbuffer.begin(),sbuffer.begin(),sbuffer.end());

        // zlib 初期化
        zs.zalloc   =Z_NULL;
        zs.zfree    =Z_NULL;
        zs.opaque   =Z_NULL;
        if(deflateInit(&zs,Z_DEFAULT_COMPRESSION)!=Z_OK){
            throw zlib_exception(zs.msg);
        }
        iflush      =Z_NO_FLUSH;
        oflush      =Z_NO_FLUSH;
    }
    // 入力コンストラクタ
    basic_zfilterbuf(std::basic_istream<Ch,Tr>& is)
    {
        out=NULL; 
        in=&is;

        sbuffer.resize(4096);
        dbuffer.resize(sbuffer.size());
        buffer  =dbuffer.begin();
        size    =dbuffer.size();
        setg(dbuffer.begin(),dbuffer.end(),dbuffer.end());

        // zlib 初期化
        zs.zalloc   =Z_NULL;
        zs.zfree    =Z_NULL;
        zs.opaque   =Z_NULL;
        zs.next_in  =Z_NULL;
        zs.avail_in =0;
        if(inflateInit(&zs)!=Z_OK){
            throw zlib_exception(zs.msg);
        }
        iflush      =Z_NO_FLUSH;
        oflush      =Z_NO_FLUSH;
    }
    // デストラクタ
    ~basic_zfilterbuf(void)
    {
        oflush=Z_FINISH;
        iflush=Z_FINISH;
        sync();

        if(out!=NULL){
            if(deflateEnd(&zs)!=Z_OK){
                throw zlib_exception(zs.msg);
            }
        }
        if(in!=NULL){
            if(inflateEnd(&zs)!=Z_OK){
                throw zlib_exception(zs.msg);
            }
        }
    }

protected:
    // setbuf
    std::basic_streambuf<Ch,Tr>* setbuf(Ch* b,int s)
    {
        if(out!=NULL){
            sbuffer.resize(0);
            setp(b,b,b+s);
        } 
        if(in!=NULL){
            dbuffer.resize(0);
            setg(b,b,b+s);
        }
        buffer  =b;
        size    =s;
        return this;
    }

    // overflow
    int_type overflow(int_type c=Tr::eof())
    {
        compress();
        if(c!=Tr::eof()){
            *pptr()=Tr::to_char_type(c);
            pbump(1);
            return Tr::not_eof(c);  
        } else {
            return Tr::eof();
        }
    }

    // underflow
    int_type underflow(void)
    {
        if(egptr()<=gptr()){
            if(iflush==Z_FINISH){
                return Tr::eof();
            }
            decompress();
            if(egptr()<=gptr()){
                return Tr::eof();
            }
        }
        return Tr::to_int_type(*gptr());
    }

    // sync
    int sync(void)
    {
        if(in!=NULL){
        }
        if(out!=NULL){
            compress();
        }

        return 0;
    }

protected:
    // compress
    void compress(void)
    {
        zs.next_in  =(unsigned char*)buffer;
        zs.avail_in =pptr()-buffer;
        zs.next_out =(unsigned char*)dbuffer.begin();
        zs.avail_out=dbuffer.size();

        for(;;){
            int status = deflate(&zs, oflush); 
            if(status==Z_STREAM_END){
                 // 完了
                out->write(dbuffer.begin(),dbuffer.size()-zs.avail_out);
                return;
            }
            if(status!=Z_OK){
                throw zlib_exception(zs.msg);
            }
            if(zs.avail_in==0){
                // 入力バッファが尽きた
                out->write(dbuffer.begin(),dbuffer.size()-zs.avail_out);
                setp(buffer,buffer,buffer+size);
                return;
            }
            if(zs.avail_out==0){
                // 出力バッファが尽きた
                out->write(dbuffer.begin(),dbuffer.size());
                zs.next_out =(unsigned char*)dbuffer.begin();
                zs.avail_out=dbuffer.size();
            }
        }
    }

    // decompress
    void decompress(void)
    {
        zs.next_out =(unsigned char*)buffer;
        zs.avail_out=size;

        for(;;){
            if(zs.avail_in==0){
                // 入力バッファが尽きた
                in->read(sbuffer.begin(),sbuffer.size());
                zs.next_in  =(unsigned char*)sbuffer.begin();
                zs.avail_in =in->gcount();
                if(zs.avail_in==0){
                    iflush=Z_FINISH;
                }
            }
            if(zs.avail_out==0){
                // 出力バッファが尽きた
                setg(buffer,buffer,buffer+size);
                return;
            }
            int status = inflate(&zs, iflush); 
            if(status==Z_STREAM_END){
                 // 完了
                setg(buffer,buffer,buffer+size-zs.avail_out);
                return;
            }
            if(status!=Z_OK){
                throw zlib_exception(zs.msg);
            }
        }
    }

private:
    std::basic_ostream<Ch,Tr>* out;
    std::basic_istream<Ch,Tr>* in;

    Ch*     buffer;
    int     size;

    std::vector<Ch> sbuffer;
    std::vector<Ch> dbuffer;

    z_stream    zs;
    int         iflush;
    int         oflush;

};

順に見ていきましょう。

コンストラクタ・デストラクタ

コンストラクタでやってるのはzlibの初期化とバッファの確保、デストラクタでやってるのはzlibの後始末ですので、特に問題ありませんね。バッファについては後述します。

sync

この仮想関数は、ストリームの対象物とバッファを同期させる必要があるとき(たとえば終了時)などに呼ばれます。入力時には特に関係ありません。このクラスでは、バッファがいっぱいになったときと同じ動作をします。

setbuf

streambufにおけるsetbufの意味合いは、Cのストリームライブラリにおけるsetvbufとだいたい同じらしいです。Stroustrupの「プログラミング言語C++ 第3版」を見てもVC++のオンラインリファレンスを見てもとてもそうとは読めないので、掌握にかなり苦労しました(笑)。

ここで、sbuffer/dbufferは、もちろんsource bufferとdestination bufferの略ですが、入出力とは関係ありません。zlibに渡すためのバッファとしてのs/dです。ですから、圧縮(出力)では非圧縮データがsbuffer、圧縮データがdbufferであり、伸長(入力)では圧縮データがsbuffer、非圧縮データがdbufferです。

実はこの時点で入出力兼用であることにかなり疑問を持ちましたが、慣用らしいのでそれにあわせたためであり、私のせいじゃないっぽいです。

一つのバッファで両方同時に処理することはできない仕様です……ってそのせいで不自然なのか。じゃ私のせいですね。気になる人は直してください。

というわけで、setbufでは、そのバッファが入力バッファであるか出力バッファであるかに応じて、ユーザからバッファが提示された場合非圧縮データの方を置き換えるようになっています。

overflow/underflow

実務はcompress/decompress関数に任せていますので、責任を果たすためのごく当たり前のことしか書いてありません。ということで、他のストリームを作成するときには雛形になると思います。

ただし、果たすべき責任を一部compress/decompressに転嫁していますので、次項を読んでください。

compress/decompress

実務はここで行っています。本稿はzlibの使い方の説明ではなくiostreamの使い方の説明なので、zlibの使い方の説明は行いわず、iostreamの使い方に絞って説明します。

ここで行われているのは、

  • たまった(使い切った)データをどうするか
  • 次のバッファの用意

です。

streambufをサーバ、ostream/istreamをクライアントとすると、overflowでは、

  • クライアントがバッファに1文字ずつ溜める。
  • 満杯になったらサーバが一気に消費する。

という動作が行われ、underflowでは、

  • バッファが空だったらサーバがバッファを満たす
  • クライアントが一文字ずつ消費

という動作が行われます。ですから、それを素直にインプリメントしてやればよろしいわけです。

でもって、溜まったデータの排出/なくなったデータの補充を行った後に、それを知らせてやっているのが、リスト中で黄色い文字で示したsetp/setgの部分です。

実は、派生元クラスやクライアントは、入力/出力バッファを、

  • 開始アドレス
  • 終了アドレス
  • 現在アドレス

からなるメモリの固まりだと認識しています。そしてそれらのアドレスを得るのに、

  • 入力バッファではeback( )gptr( )egptr( )メンバ関数
  • 出力バッファではpbase( )pptr( )epptr( )メンバ関数

が使われます。これらのメンバ関数が何を元に値を返すかといえば、それがsetp/setgで与えた情報、というわけです。ですから、setp/setg呼び出しではそうなるように引数を渡せばよろしい。

basic_zfilter

クライアントのほうは例によって超簡単です。

// 非圧縮→圧縮
template <class Ch,class Tr=std::char_traits<Ch> >
class basic_ozfilter : public std::basic_ostream<Ch,Tr> {
public:
	basic_ozfilter(std::ostream& os)
        : std::basic_ostream<Ch,Tr>(new basic_zfilterbuf<Ch,Tr>(os))
	{
	}

	~basic_ozfilter(void)
	{
		flush();
		delete rdbuf();
	}
};

// 圧縮→非圧縮
template <class Ch,class Tr=std::char_traits<Ch> >
class basic_izfilter : public std::basic_istream<Ch,Tr> {
public:
	basic_izfilter(std::istream& is)
        : std::basic_istream<Ch,Tr>(new basic_zfilterbuf<Ch,Tr>(is))
	{
	}

	~basic_izfilter(void)
	{
		delete rdbuf();
	}
};

typedef basic_ozfilter<char> ozfilter;
typedef basic_izfilter<char> izfilter;

こんなカンジ。

これらのソースは、VAFX最新版にVAFXZlib.hppとして含めてあります。

バッファリング編その2・base64を使ってみる

ある程度構造の存在するデータ形式を作ろう、なんてときに、テキスト形式を検討することがありますね。チェックが簡単だからです。

そんなときにしばしば問題になるのが、メモリイメージに毛が生えた程度の物をどうするか、です。

例を挙げるなら、ビットマップなどです。通常ビットマップ単体ならばバイナリ出力すればすむ話ですが、他のデータを含む構造化されたファイルを作りたい、でもファイル分割するのはいやだ、なんてときにはとても困ってしまいます。

こんなときには妥協案として、バイナリデータをテキストエンコーディングしたデータをファイルに含めてしまいましょう。Eメールの添付ファイルみたいに、です。

バイナリ←→テキスト変換には昔からuuencodeとかishとかありますが、ここではRFCのbase64規格を使ってみましょう。

もちろん、真の入出力先を固定するのはもったいないので、これも例の「フィルタ」形式で実装します。

 

……ここにソースを載せようと思ったのですが、長いので止めておきます。VAFX最新版にVAFXB64.hppとして含めてありますので、それをご覧ください。長いのは、zfilterではzlibに任せていたエンコーディング処理が組み込まれているからですね。でも、各部の意味はzfilterと変わらないので特に問題ないと思います。


(C) 1999 Naoyuki Hirayama. All rights reserved.