C++ iostream 実装例 その 5 †さて、ここまでの実装でも十分実用になると思われるが、 ここまできたら fstream のように、iostream から派生 した class までに仕上げてしまいたい。ここから先は streambuf からは少し離れて、iostream 化を 目指そう。 constructor をどうするか †ここでの問題に限らず、C++ で新しい class を作る時に、 constructor をどうするかは、常に悩ましい問題である。 ここまでの tcpbuffer の実装は、「すでに connect された socket stream」ありきでやってきた。つまり、接続を確立 する話と buffer 入出力の話を完全に分離し、後者のみに 特化してきた。そのおかげで、定義は著しく簡単になっている。 一方、利用する側からすると、接続の確立と buffer 入出力は 一体化しておいて欲しい。接続を確立していないものを tcpbuffer に渡してしまうかも知れないし、渡すつもりの socket とは別の socket を渡してしまうかも知れない。 C++ は class という概念を導入し機能と data を一体化して、 このような過ちを防ぐ手段を提供してくれているのだから。 この二つを一体化した class という意味は、いくつか考えられる。
この違いが constructor をどう作るかの違いになる。 一番目が従来の C program 的発想から来るもので、この場合、socket descriptor は constructor の時点では invalid でよい。例えば connect() などの関数で 初めて接続を試み、確立した socket にする。fstream の open() と同じ発想である。 二番目も同様であるが、socket を隠蔽した class を作る。必要最小限の機能のみを 公開関数とすることで、socket に対する細かな操作が隠蔽され他の object を 使う場合も対応可能になる。 三番目が、ここで今までやってきたものに1番近いもので、「connect した stream」 という class を作る。socket descriptor のみならず「接続する」ということ自体が 隠蔽されるので、buffer 操作のみに専念できる。 筆者が三番目が好みであることは、ここまでの実装の仕方を見ればあきらかであろう。 だが、現実はなかなか厳しい。更に言えば、ユーザの人気という意味では実は一番目が 一番人気であろうということも知っている。まずは、筆者の愚痴から始めよう。 connect した stream の class †これが一体どういうものかというと、生成された時から接続が 確立している class である。つまり、constructor で接続の確立までしてしまう。 前節の使用例では接続するのに host と port を使った。従って、例えばここでは constructor にはこの二つを 渡すものとしよう。 例えば class 名を sockstream とすると sockstream strm(host, port); としただけで、接続完了。あとは基底 class である iostream が 提供する機能があれば十分。 close() は直接外には出さない。destructor でやる。 つまり、生成された時が接続が開始された時であり、消える時が接続が 閉じられる時である。Scope 中にこの object があれば接続があるのであり、 無ければ接続は無い。 実際、こういう書き方でいくつか appication を書いてみたが、著しく program の 見通しがよくなる。なにしろ object の在る無しが接続の在る無しなのだから。 問題は、、 †だが、もちろん便利なものには落とし穴もある。 まず第一に接続できない時にどうすればよいのか? もとの思想を厳密に解釈するなら、その object が自分で消えなければいけない。 そんな仕掛けは C++ にはない。せいぜい constructor の中から例外を投げる ことであろう(ただし生成されてから消えるのではない。あくまでも object が 生成されないだけである。生成されないだけなので、destructor は呼ばれない)。 ところが例外というのは 投げる方は簡単だが、投げられた方はたまったものではないということが多い。 なにしろ try block の途中で突然、その try-block 全部が無かったことに されるのだから。 もしその例外が、設定がうまくできなかった(例えば host 名が間違っていた) ことが原因で、かつ(ユーザに問い直すことができるとかで)その修正が 即座に可能である場合、try-block によるご破算の影響を最小限にするためには、 細かく何段にも try-catch を入れるというのもあろうが、 そうなると object の state を取得して、if で分岐するとか、 switch で分岐するのと大差無くなる。 第二の問題は、例外を throw するのであれば、例外の詳細を 決めておかなければならない、または throw する例外に詳細情報を入れて おかねばならないことである。 constructor が例外を throw した場合、その object は生成されないし、 construct した後であっても、try-block から抜けた段階で一般に object は destruct されている(new を使って生成していれば別だが)。従って 例外を処理する側は throw された例外 object の情報しか使えない。 つまり、生成する object とは別の object(例外 object の方) に、例外処理に必要となると予測されるすべての情報を持たせる必要がある。 一般に、例外を throw するのは、その object の段階では、その後どうして よいかわからないから throw するのであって、例外処理に必要と予測される すべての情報というのは、決められないことが多い。 fstream の場合 †iostream の典型例として fstream がある。 いろいろあれこれ悩んでも、smart な解が見つからないので、 これを調べてみよう。 fstream には、2種類の constructor がある。
いずれの場合も、これら constructor が例外を投げることはない。 では、2番目の constructor の場合で、何らかの理由で open できなかった時 どうするか?答えは fail bit を立てる(bad bit も立っているかも知れない)。 fail bit が立つと、void* は 0 を返し、bool operator! は true を返すように なる(ように実装されている)。つまり、条件分岐による処理が可能である。 更に、例外機構を使うこともできる。 exception 関数を使って、fail bit が立った時に、例外を発生させる こともできる。ただし、この設定ができるのは object が生成された後である ので、constructor の段階で例外が投げられることはない。 というわけで、例外機構の利用は user の選択に任せる、ただし constructor での 例外送出はしない、その代わり2段階(object 生成と open )生成も可能にする (open の例外を open 時に捕捉したければ2段階生成を使う)、という妥協案である。 fstream の例外 †fstream の例外は、ios_base::failure object を投げる。 これは基底 class ios の機能で、ios::exceptions() の設定に従い、例外を 投げる。ios::exceptions() で設定できるのは、
の3つ(の組み合わせ)である。しかし、一般に failbit が立つだけの場合が多く、 どのような例外であるのか区別はむつかしい。 例を挙げておこう。 #include <fstream> #include <iostream> int main(int argc, char* argv[]) { std::fstream fstrm; fstrm.exceptions(std::fstream::badbit | std::fstream::failbit); try { fstrm.open(argv[1], (std::ios::in | std::ios::binary)); char c; while(fstrm.get(c)) std::cout << c; } catch(std::fstream::failure& e) { std::cerr << "Failure: " << e.what() << std::endl; } return 0; } この例の場合、例外が throw されるのは、open() が失敗した場合と、get() が 失敗した場合(一般に gcount() == 0 となる読み込みは failbit が set される ので、eof() に達した時は eofbit だけでなく failbit も set される)である。 この例では、どちらで例外が発生したか、わからない。ただし、fstream object は try-block の外に置かれているので、消滅していない。 従って catch-block の中で rdstate() などを使って調べることは可能である。 なお、例外を発生させないよう設定するには
を設定する。 |