追記: 特に修正を加えずこのままにしておきますが,コメントの指摘内容と私の回答内容がズレてるかもしれません(http://d.hatena.ne.jp/tt_clown/20100513/1273746825#c 参照).
「C++ は難しいから」と言う定説 - Life like a clown に付いたコメントへの返信的な記事.ずっと放置しておいて,今さらと言う感じですが.
イテレータと言う抽象化とストリームの扱い
実際にC++からJavaを触った人が多くの点でよくなっているところを実感できているんじゃないだろうか。C++は変な構文もちょくちょくあって、
ifstream fi("x"); vector<int> d(istream_iterator<int>(fi), istream_iterator<int>());あれ?何がおこってるのとかってなったり、
http://d.hatena.ne.jp/tt_clown/20090708/p1#comment
>>javaならこーいうこともしなくてもいいし。
stream_iterator(および streambuf_iterator)は,C++ において初見時に戸惑うものの一つだろうと思います.私自身も,ある程度慣れるまではなかなか stream_iterator を有効に利用することができませんでした(stream_iterator を使うと言う発想になかなか至らなかった).それ以外では,insert_iterator (, front_insert_iterator, back_isert_iterator) も戸惑いました.そう考えると,確かにイテレータは初学者泣かせな機能なのかもしれません.
ジェネリック・プログラミングが普及するにつれて,データ形式とアルゴリズムはできるだけ分離して記述しようと言う動きが加速しました.STL においても,様々なアルゴリズム「だけ」を提供するライブラリである
この規則は,配列や各種コンテナ (vector, deque, list, ...) の場合には直感的にも分かりやすい形で適応させる事ができます(配列の各要素をイテレータに対応させる).しかし,ストリームに関してはなかなかそう上手くはいきません.ストリームは「切れ目のないひと繋がりのデータ」を表し,これをどのような単位に分割するかは一意に決定することはできません.例えば,上で挙げられていた「変な構文」は以下のように書くことができれば初学者にとって「直感的で分かりやすかった」のかもしれませんが,この動作を一意に決めることはできませんでした(もしくは,一意に決めてしまうと不便になる).
ifstream fi("x"); vector<int> d(fi.begin(), fi.end());
そこで,C++ では stream_iterator と言うクラスを通じてユーザに,切れ目のないストリームに対して処理を施す単位を決定してもらうと言う形を取りました.
「ストリーム」を(どのような単位に区切るかを含めて)どのように処理するかと言うのは,その時々の要求に依るところが大きいので,プログラミング言語に関わらず記述方法はどうしても複雑(トリッキー)になりがちです.例えば,Java における以下の記述は初学者を戸惑わせる例の一つだろうと思います.
// UTF-8 形式のファイルを読み込む. FileInputStream ifs = new FileInputStream("foo.txt"); BufferedReader reader = new BufferedReader(new InputStreamReader(ifs, "UTF-8"));
視点・立場の違い
個人的な経験則ですが,stream_iterator などの利便性を実感できるのは「自分が何らかの汎用性のあるアルゴリズム(関数,クラス)を提供する」側になったときだろうと思います.私が初めて streambuf_iterator(および streambuf_iterator)の利便性に気づいたのは,html での特殊文字をエスケープするための関数を書いたときでした.
標準入力から一文字ずつ読み取り,変換しなければならない文字なら対応する文字列を出力し,それ以外の文字はそのまま出力するという流れになっています.ここで面白いのが,src2htmlはイテレータで受け取っているという事.このプログラム,メイン関数を以下のように書き換えても問題なく動作します.
Streambuf Iterator - Life like a clown
・・・(中略)・・・
つまり,制御関数(src2html)を実装する際には,イテレータを用いることによって,メイン関数からの入力(文字列が来るのか,ストリームから読み取らなければならないのか)を意識せずに実装することができます(実際,algorithmヘッダファイルで提供される関数もこの形で実装されています).
何らかの関数(アルゴリズム)を記述する際に,「入力として引数に指定されるのは文字列かもしれないしファイルかもしれないし標準入力かもしれない」のような事例にはしばしば遭遇します.C++ ではこのような場合に,関数の引数(インターフェース)をイテレータにしておく事によって,「文字列版とストリーム版を別々に用意する」のような煩わしさから開放されます(各イテレータが,その煩わしさを肩代わりしてくれる).
以前に Boost.勉強会に参加したときに耳にした「Library for libraries」と言う言葉が非常に印象的でした.Boost に収録されているライブラリを含め,世の中で提供されているライブラリは必ずしも「最終的なユーザ・プログラマ*1」に向けて提供されているものばかりではないと言う事を再認識しました.ライブラリと呼ばれるもの中には「汎用性のあるライブラリを記述するための補助的な役割」としてのライブラリも数多く存在します.
多くの場合、全ての言語機能には「理由」があるのだが、それに対して「なぜ」ではなく「糞」と片付けてしまう。
「俺のソースだから」というプログラマは死んだらいいのに - 神様なんて信じない僕らのために
あくまで「自分が使って不都合だと感じたらその機能は糞」なのだ。
他の理由があってそうなっているとしても。
一見すると不可解な記述になっている部分と言うのはほとんどの場合,何かしらの理由が存在します.個人的な印象では,この「理由」は「ライブラリを提供する側と使用する側双方の利便性を検討した結果の落としどころ」に起因するものが多いように感じられます.そのため,「何でこんな記述になっているのか分からない」と感じた場合は,視点(立場)の違いを疑ってみると良いのかもしれません.
利便性を理解できない(実感できない)間はその機能を使わずにプログラミングを行うのも良いと思います.ただ,「(今は分からないけど)そうなっているのは必ず何らかの理由があるはずだ」と言う意識だけは捨てないように気をつけると良いかなと思いました(この辺は自戒でもありますが).
*1:メイン関数を書いてアプリケーションを作成するプログラマ?