STLスタイルのバイナリファイルを操作する

高校生と後輩のプログラミングを教える過程で生じた1つの問題の解決策についてお話ししたいと思います。 当然、私はこの経験をより多くの聴衆に興味があると思うので、これについて書いています。



問題の声明



少なくともロシアでは、バイナリファイルの操作はプログラミング教育の伝統的なトピックです。 最後の役割は、ロシアの学校で広く普及しているPascalプログラミング言語では果たされていません。これは、いわゆる型付きファイル( file of integer



file of integer



種類file of real



file of integer



種類など)をサポートする組み込みサポートを備えていfile of real



。 場合によっては(学童または中学のコースでのプログラミングの詳細な研究で)、C ++言語の研究が始まると、この言語の新しい環境で「タイプTのファイル」を処理する問題を解決したいという要望があります。 そして、ここで疑問が生じます。これにはどのような手段を使用する必要があります。



残念ながら、C ++言語のバイナリファイルを操作するための低レベルツールのみが提供されています。標準のistream



/ ostream



ストリームタイプのread



/ write



メソッドです。 他の明らかな欠点に加えて、この事実はSTLスタイルのプログラミング(つまり、アルゴリズムとイテレーターに関連する標準C ++ライブラリの一部)を完全には利用していません。



したがって、タスクは、STLのシーケンス( vector<T>



など)と同様に、タイプT



の値のシーケンスを格納するバイナリファイルで作業を提供することです。 T



は、基本型の値、およびいわゆるPOD型(PODの概念に精通していない場合は、基本型のみを考えることができます) T



意味します。



可能な解決策



ios_base ::バイナリ:失敗#0


このようなタスクに遭遇したことがない場合(奇妙なことです!)、 ios_base::binary



フラグについて何か思い出すことができますが、よく会った人はこのツールはほとんど役に立たないことを知っています。 言語標準は、ストリームを開くときにこのフラグを示すことからどのような効果が期待できるかについて非常に簡単ですが、ネットワークには、プラットフォームに依存する改行文字と、場合によっては他の文字のブロードキャストを単に無効にするという説明がありますタスクに直接関連しています。



静的多型:失敗#1


ほとんどの場合、基本レベルよりも少し深い標準ライブラリを知っている人は、 ofstream



/ ifstream



ファイルストリームの標準型は、 basic_ofstream



/ basic_ifstream



明示的なインスタンスの同義語であることを覚えているでしょう。 これらのテンプレートには、ストリームの文字タイプ(このパラメーターをCh



と呼びましょう)と文字タイプの文字タイプの2つの類似したタイプパラメーターがあります。デフォルトでは、これはstd::char_traits<Ch>



です。 ofstream



/ ifstream



char



型はCh



として取得されます。



これは、テンプレートファイルCh



値として指定して、バイナリファイルから読み取りたいタイプT



でこれらのテンプレートをインスタンス化しようとする考えをすぐに提起します。 このタイプのストリームからint



値を読み取ろうとする最も単純なコードは、 bad_cast



。 おそらく、 char_traits<int>



特殊化をchar_traits<int>



し、2番目のパラメーターをファイルストリームクラステンプレートに渡すことで何かを変更できたかもしれませんが、この方法は期待できなかったようです(各タイプT



非常に広範なchar_traits



テンプレートの特殊化を記述しています...)さらに彼に対処します。



OOPと動的ポリモーフィズム:失敗#2


最初の失敗の後、標準ツールから「完全に無料」の目的の動作を得ることができず、いくつかのコードを記述する必要があるという結論に達する可能性があります。 OOPパラダイムでこの問題を解決してみましょう。つまり、いくつかのクラスを作成します。 もちろん、フローのクラス。 標準のofstream



/ ifstream



から継承され、祖先からの最大の定義を保持し、何が起こるかを確認します。 (括弧内では、このタスク自体に意味がないわけではないことに注意してください。なぜなら、B。Straustrupの本の演習のリストでかなり高い評価の複雑さでマークされているからです。 C ++ "。)



最初から、ストリームクラスの<<



および>>



操作をオーバーロードする必要があることは明らかでした。 これで十分のようでした。 以下で問題が発生しました。 標準ライブラリのアルゴリズムを使用してストリームを操作するには、I / Oイテレーターを使用する必要があります。 STLの作成者によると、彼女のツールはすべて一般化されており、私のストリームクラスがいくつかの暗黙的なライブラリ要件を満たすとすぐに、彼女は喜んで動作することを期待していました-静的ポリモーフィズム...特に、標準のイテレータがストリームのタイプによってパラメータ化されると予想しました、彼らと一緒に働いています。 あった! 標準のbasic_ofstream



/ basic_ifstream



型はbasic_ofstream



であり、入力/出力反復子パターンの定義にbasic_ifstream



れています。



オペレーション<<



>>



実装の1つの機能に注意を払うと、このパスに沿った救いの希望が残り>>



。基本タイプの場合、これらはストリームクラスのテンプレートのメンバー関数として実装されます。 さらに、それらが仮想と宣言された場合、動的ポリモーフィズムに依存する可能性があります(標準イテレータは基本クラスへの参照によってフローのオブジェクトを格納します)-元の問題の部分的な解決策は機能し、基本型(int、doubleなど)。 ただし、これらのメンバー関数は仮想ではありません。 ここでは、標準ライブラリデバイスのロジックまたはその欠如を推測できます(たとえば、STLの場合、元々OOP、継承、およびポリモーフィズムのすべてのパワーを使用することを意図していないことが知られていますが、ストリームライブラリはOOPに基づいて構築されています...)、しかし、最終的なソリューションに進みます。



アドホックポリモーフィズム(オーバーロード):勝つ



最後に、必要なのは、特別なバージョンの<<



および>>



操作を呼び出すwrite



です。これにより、 read



/ write



介したファイルの低レベルの作業が隠されます。 これらの操作のオーバーロードを提供し、呼び出されることを確認するだけで十分です。 これは、引数に特別な型を使用することで実現できます。 フローのタイプを操作することができなくなりました-入力/出力用の特別なタイプを思い付くことが残っています。 これは、「ラッパー」と呼ばれるものの使用を頼みます。



幸いなことに、さまざまなT



型の新しいラッパークラスを記述する必要はありません。型パラメーターT



フィールドを格納し、このフィールドへのリンクに変換できる1つのクラステンプレート(定数および非定数)に制限できます。 explicit



に宣言されていない、型T



1つのパラメーターを持つコンストラクターにより、型T



値を暗黙的にラッパー型に変換できます。 パラメーターのないコンストラクターはSTLの要件です。 結果のコードを以下に示します。

 #include <iostream> using std::istream; using std::ostream; template<typename T> class wrap { T t; public: wrap() : t() {} wrap(T const & t) : t(t) {} operator T&() { return t; } operator T const &() const { return t; } }; template<typename T> istream & operator>>(istream & is, wrap<T> & wt) { is.read(reinterpret_cast<char *>(&static_cast<T &>(wt)), sizeof(T)); return is; } template<typename T> ostream & operator<<(ostream & os, wrap<T> const & wt) { os.write( reinterpret_cast<char const *>(&static_cast<T const &>(wt)), sizeof(T)); return os; }
      
      





static_cast



を使用するには、コンパイラがクラステンプレートの本体で定義された型キャスト操作を呼び出して情報フィールドへのリンクを取得し、 static_cast



がこのフィールドreinterpret_cast



アドレスをchar



へのポインターにreinterpret_cast



必要があります。



ラッパーの使用方法を示す例を次に示します。 最初に定められたアイデア、つまりSTLスタイルのプログラミングの痕跡があります。

 #include <algorithm> #include <fstream> #include <functional> #include <iostream> #include <iterator> #include <numeric> #include <cassert> int main() { int arr[] = {1, 1, 2, 3, 5, 8}; //  std::ofstream out("f.dat"); std::copy(arr, arr + 6, std::ostream_iterator< wrap<int> >(out)); out.close(); // : ,       std::ifstream in("f.dat"); assert( std::inner_product( std::istream_iterator< wrap<int> >(in), std::istream_iterator< wrap<int> >(), arr, true, std::equal_to<int>(), std::logical_and<bool>()) ); }
      
      





おわりに



結果が「絶対バイク」であることは明らかです。これはおそらく多くのC ++プログラマーによって書かれたものですが、ネットワーク上または一部の有名なライブラリ(たとえばBoost、特にBoost.Iostreams)でこれに気付きませんでした。



また、タスクの関連性に関する議論を意図的に省略したことにも注意したいと思います。 おそらく、 read



/ write



よりも高いレベルでファイルを操作することを想像できない人がいるでしょう。 おそらく、バイナリファイルは過去のものであり、非常に移植性が低いか、類似のものであると言う人がいるでしょう。 たぶんこれは部分的には本当かもしれませんが、そのような問題を解決するための演習自体は私にとって興味深く有益なものでした。



問題の声明と解決策の議論について、Vitaly Nikolayevich Bragilevskyに心から感謝します。



UPD1 :コメントで、タイプ変換の代わりに、通常のgetメンバー関数を書く理由を尋ねました。 変換は、基本的に次のような例で使用されます。

  std::ifstream in("f.dat"); int arr2[6]; std::copy(std::istream_iterator< wrap<int> >(in), std::istream_iterator< wrap<int> >(), arr2);
      
      






All Articles