Last update 1999/09/22
(C)平山直之
無断転載は禁止、リンクはフリー
誤字脱字の指摘は歓迎
例によって唐突ですが、iostreamの拡張について書いてみたいと思います。ここでいう「拡張」とは、自分でcinとかcoutみたいなものを作ろう、ということです。
「こんなもん一番最初に教えんなや」と比較的評判の悪いiostreamですが、
- 演算子オーバーロードがいわば「御法度」な使い方をされている
- メソッドの命名法がやたら変
などのように表面的に美しくないのが悪評の原因で、その辺に目をつぶれば実はそれなりによくできたシステムだったりします。
拡張方法も一度覚えると結構便利ですぜ。
iostream、というと、そのインターフェイス(Java的な意味じゃなくて)であるところの
std::cin
、std::cout
などを思い浮かべるのが普通ではないかと思います。ここでiostreamを拡張して
cprinter
なんてものを作りたくなったとしますよね。その場合、std::cin
(std::cout
)かその基底クラスあたりを派生して、何か適当な仮想関数をオーバーライドすると考えるのが普通ではないかと思います。でも、実は違うんですね。といっても、iostreamがそういう「通常の予測」を裏切るのは、非直感的な悪い設計になっているからではなくて、普通の人が考えるよりもスマートな設計になっているからなんです。
では実際にどのようになっているかというと、
basic_istream
/basic_ostream
(それぞれcin
、cout
の基底クラス)は単なるライブラリユーザのためのインターフェイスにすぎない- データのバッファリングおよびデバイス等への入出力は、
basic_streambuf
の派生クラスで行うという決まりになっているのです。
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プログラマから見るとこれはちょっと変に見えます。どこが変かといえば、
- 何よこのウィンドウは。おれウィンドウ出せなんていってないぜえ。
- 標準出力がこのウィンドウに結び付けられてんの? 大体キャラクタ端末だなんて誰が決めたのよ
- 大体WindowsのアプリケーションはWinMainで始まるんじゃなかったのかよ
なんてカンジです。
これらの鍵はもちろん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);
としておきます。
seekoff
、seekpos
これらの仮想関数に関しては、そのストリームバッファがシークをサポートしない場合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は
basic_ofstream<char>
→ofstream
basic_ifstream<char>
→ifstream
のように行われていますから、ここでもやっておきましょう。
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デザインパターンの適用などを検討してみましょう。
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として含めてあります。
ある程度構造の存在するデータ形式を作ろう、なんてときに、テキスト形式を検討することがありますね。チェックが簡単だからです。
そんなときにしばしば問題になるのが、メモリイメージに毛が生えた程度の物をどうするか、です。
例を挙げるなら、ビットマップなどです。通常ビットマップ単体ならばバイナリ出力すればすむ話ですが、他のデータを含む構造化されたファイルを作りたい、でもファイル分割するのはいやだ、なんてときにはとても困ってしまいます。
こんなときには妥協案として、バイナリデータをテキストエンコーディングしたデータをファイルに含めてしまいましょう。Eメールの添付ファイルみたいに、です。
バイナリ←→テキスト変換には昔からuuencodeとかishとかありますが、ここではRFCのbase64規格を使ってみましょう。
もちろん、真の入出力先を固定するのはもったいないので、これも例の「フィルタ」形式で実装します。
……ここにソースを載せようと思ったのですが、長いので止めておきます。VAFX最新版にVAFXB64.hppとして含めてありますので、それをご覧ください。長いのは、zfilterではzlibに任せていたエンコーディング処理が組み込まれているからですね。でも、各部の意味はzfilterと変わらないので特に問題ないと思います。
(C) 1999 Naoyuki Hirayama. All rights reserved.