(このコンテンツはメールマガジンの STL & iostream 入門に手を加えたものです。「 STL と iostream が使えるかのチェック」等はメールマガジンの方のページをご覧ください)
|
STL & iostream ( #25 ) |
「 STL & iostream 入門」の締めくくりとして、 STL と iostream を使用した一般的な例を見てみましょう。
まず、こんなファイルを用意します。 //////////////////////////////////////////////////////////////// // Data.txt の中身。 53.5827 48.1754 42.5779 36.8125 30.9017 24.869 18.7381 12.5333 6.27905 0.00218 -6.27905 -12.5333 -18.7381 -24.869 -30.9017 -36.8125 -42.5779 -48.1754 -53.5827 -58.7785 ///////////////////////////// Data.txt には実数値のデータが1行ずつ入っています。このデータを読み込んで逆順にするのが、次のプログラムです。 //////////////////////////////////////////////////////////////// // ファイルから実数データを読み込んで逆順にして書き込みます。 #pragma warning( disable : 4786 ) // VC での警告を取り除きます。 #include <iostream> #include <fstream> #include <algorithm> #include <vector> const char g_pchDataFile[] = "Data.txt"; typedef std::vector< double > d_vector; bool ReverseStrmData(); bool Load( std::istream &p_rcIStrm, d_vector &p_rcDVec ); void Save( std::ostream &p_rcOStrm, const d_vector &p_rcDVec ); // 各関数を呼び出します。 bool ReverseStrmData() { std::fstream cFStrm( g_pchDataFile, std::ios::out | std::ios::in ); if( cFStrm.fail() ) return false; d_vector cDVec; bool bRes; // ストリームから読み込みます。 bRes = Load( cFStrm, cDVec ); if( bRes == false ) return false; // 最後まで読み込んだので初期状態に戻します。 cFStrm.clear(); cFStrm.seekp( 0 ); // 逆順にします。 std::reverse( cDVec.begin(), cDVec.end() ); // ファイルに書き込みます。 Save( cFStrm, cDVec ); return true; } // istream からデータを読み取ります。 bool Load( std::istream &p_rcIStrm, d_vector &p_rcDVec ) { typedef std::istream_iterator< double > CIStrmIterD; std::copy ( CIStrmIterD( p_rcIStrm ) , CIStrmIterD() , std::back_inserter( p_rcDVec ) ); if( p_rcDVec.empty() ) return false; return true; } // ostream へとデータを書き込みます。 void Save( std::ostream &p_rcOStrm, const d_vector &p_rcDVec ) { typedef std::ostream_iterator< double > COStrmIterD; std::copy ( p_rcDVec.begin() , p_rcDVec.end() , COStrmIterD( p_rcOStrm, "\n" ) ); } ///////////////////////////// 各関数について見てみましょう。 ReverseStrmData() 最初に呼ぶ関数です。 まず std::fstream 型変数からファイルを開きます。またデータを格納する d_vector 型変数を作ります。 d_vector は std::vector< double > を typedef したものです。 次に各処理を行う関数を呼びます。 Load() は次の解説を読んでください。 Load() したあとはファイルポインタが最後まで来ているので、それを初期化しておきます。 std::reverse() は STL アルゴリズムのひとつで、渡された配列やコンテナの各要素を逆順にします。 最後に Save() を呼んで終了。この関数についても以下を見てください。 Load() 第1引数の std::istream から実数値を取り出して第2引数の d_vector コンテナへとコピーします。 std::istream を引数として受け取るのは、この関数を呼び出す側で std::fstream 以外も渡すことができるからです。 UNIX 系では、ファイル名を指定してではなく、標準入出力とリダイレクトを使ってファイルへの処理を行う場合があります。そういう機能を設ける場合には、こういった設計が後々生きると思います。 std::istream から値を取り込みコピーするのに、 STL の機能をフルに活用します。 std::istream 側には std::istream_iterator という std::istream 専用イテレーターを使用します。このイテレーターは std::istream からテンプレート引数の型として値を読み取ります。この値は * 演算子で外からアクセスできます。引数には std::istream 型変数を渡します。「ストッパー」にするイテレーターは、引数を省略して渡します。こうすると、 fail() が立つまで読み続けます。 std::istream_iterator の簡易版を #17 で作ったので読み返してみてください。基本的な仕組みは同じです。 コンテナ側は std::back_inserter() を使って、読み取った値をコンテナへと追加していきます。これは #21 を見てください。 このふたつのイテレーターを std::copy() に渡してコピーします。ふたつのイテレータークラスが、 STL アルゴリズムの std::copy() を使えるようにサポートしているということですね。 Save() 第1引数の std::ostream へと、第2引数の d_vector コンテナの各要素をコピーします。 コピーは std::copy() を使用します。 コンテナ側は普通に begin() と end() を使用します。 std::ostream 側は std::ostream_iterator を使用します。このイテレーターはテンプレート引数の型として値を書き込んでいきます。第1引数には std::ostream 型変数、第2引数には「各値を区切るための文字列」を渡します。 以上のようなプログラムが、 STL と iostream の基本的な使用方法、と言えるでしょう。 皆さんは、必要に応じてこれを拡張していけばいいわけです。自分で作ったクラスを使いたい場合には、そのクラスに必要な演算子を加える。元々あるクラスを使いたい場合には、グローバルな演算子関数や関数オブジェクトを作る。データに対して特殊な処理をしたい場合には、アルゴリズムや関数オブジェクトを新たに作る。普通じゃないデータ(ウィンドウとか)に対して処理する場合にはイテレーターやカスタムバッファを作る。 そして、自分が作ったものをテンプレート化したりすることで汎用性を持たせて、まるで STL や iostream の一部であるかのように使う。その繰り返しを続けていくことになるでしょう。 STL と iostream が提供するのは、機能性の高いクラスや関数よりも、それぞれのクラスや関数が柔軟性を保ちつつ結びついている、その枠組みとしての意味の方が大きいでしょう。 STL と iostream を活用する、ということはその枠組みを最大限利用する、ということなのです。 |
テンプレートとの付き合い方 ( #25 ) |
ここまでで STL と iostream の「入門段階」については終了です。ここまでで、全体の「3分の1」に届いたかどうか、くらいです。アロケーター、トレイツ、ローカライズなど、まだまだ奥深いところに様々なパラメーターが存在します。使いこなすには、さらに多くのことを学ぶ必要があるでしょう。しかも、これも標準 C++ ライブラリ全体には満たないのです。
ただ、標準 C++ ライブラリは、表層だけでも使う価値のあるライブラリですし、十分使いやすい構造になっていることはこれまで読んできて分かってくれたと思います。少しずつ、自分の実力に合わせて、深く理解していき、カスタマイズしていってください。 最後に、「壁になるであろう部分」について書いておきます。 1:とにかくコンパイルエラーが出る 最初はこれに悩まされると思います。まずできるだけ STL や iostream を使わずにコードを書いて、それで通ることを確認しましょう。そこから少しずつ加えていくのがいいと思います。 このライブラリはテンプレートを多用しているので、コンパイラが処理しきれないためにエラーになる、という場合もあります。そういった場合には、さまざまなコンパイラでチェックしてみるとこをお勧めします。 あとはやはり、 C++ の知識を深めて、ライブラリのコードをよく読むことでしょう。 2:実行結果が想像と違う まず STL も iostream も、いくつかのベンダーから様々なバージョンがリリースされています。なので、他の標準 C++ ライブラリで同じような現象が起きるか試してみましょう。 起きた場合にはプログラムミスです。「間違った使用方法をしていないか」や「自分の想像した関数が呼ばれているか」「自分の想像したテンプレート引数が指定されているか」などをチェックしてみてください。この場合には、強力なデバッガを持つ開発環境を使用するのが一番でしょう。 3:コードが読めない 標準 C++ ライブラリを理解する一番の方法は、ライブラリの中身を見ることです。でも、最初は難しすぎて読めないかもしれません。 まずは C++ の知識を深めて、どんな構文も理解できるようにしましょう。C++ 独特のスタイルがこのライブラリでは頻繁に出てきます。これに慣れることが必要になるでしょう。 次は、デバッガで追いかけてみましょう。特に iostream は、汎用性を重視しすぎているからか、無駄な「まず呼ばれない」コードが結構あります。そういった部分を無視することで理解しやすくなるでしょう。 最後の手段はライブラリを書き換えてしまうことです。標準 C++ ライブラリはその大部分がテンプレートなので、ソースコードがそのままライブラリになっています。バックアップを取ったあと読みやすいように書き換えたり、 printf() を書き加えたりしてしまいましょう。 少なくとも、僕はかなり苦労しました。「できる人はすらっと読める」というもんでもないと思うので、こつこつ理解していってください。 4:関数やクラスが作れない どんなに理解できても、実際に書けないという人は結構いると思います。これについては様々なアプローチを試みるのがいいと思います。 まずは、元々あるものをコピーしてきて拡張するという方法に挑戦してみてください。自分が作る部分を極力減らすことで、部分部分での「書く能力」を身に付けていきましょう。また「読みにくいコードを読みやすいスタイルに書き換えて、注釈を書き加える」ことで、コードの部分部分を解析していく方法も有益でしょう。 次は、見ずに打ち込めるように練習すること。「見ずに」というのは必ずしも「暗記して」という意味ではありません。たとえば「 < 演算子の代わりになるプレディケートを作る」というテーマを持って、どういう仕組みになっているかを思いだし考えながら自分で打ち込んでみてください。最初何度かは、元のコードを見てもいいでしょう。でも最終的には何も見ずに打てるようになるのが理想です。こうすることで「全体の仕組み」が自然と頭に入っているものです。 最後は「全部自分で作ってみる」ことです。試行錯誤しつつ作っていくということは非常に有益だと思います。この過程で「自分が理解していない部分」を見つけだして、そして補っていってください。 STL と iostream の難しさは「全体構造の理解のしにくさ」「 C++ の文法の難しさ」そして「積極的に自分で関数やクラスを作る必要がある」という点に尽きるでしょう。 これまでの標準 C ライブラリが「ゲーム専用機」なら、標準 C++ ライブラリは「自作パソコン」です。それぞれのパーツを理解し、繋ぎ合わせ、必要なら新しくパーツを用意する。難しいのは当然と言えます。でも必ずしも「自作パソコン」が難しくないように、標準 C++ ライブラリもパズル的な仕組みさえ理解してしまえば、そこに広がるのは無限の可能性です。「ゲーム専用機」には真似できない世界があるのです。 ま、そんなこと言う必要はないのかもしれませんね。 皆さんはもう、標準 C++ ライブラリの魅力に取り憑かれてしまったに違いありませんから……。 |
(C)KAB-studio 2001 ALL RIGHTS RESERVED. |