実践C++入門講座こんにちは。難しい話が続いたので、ちょっと気楽にfor文について解説します。C++ではC言語にはなかった「イテレータ」が追加されました。C++11ではコードがスッキリして可読性が上がる範囲ベースforを使えるようになりました。今回はこれらの使い方だけでなく仕組みについても解説します。各種のコンテナに保存されている要素を枚挙するために設計されたクラスがイテレータです。使い方はポインタによく似せられていますが、実体はクラスです。(各コンテナの内部で定義されています。std::vectorの内容をポインタで枚挙する時は次のように書くことができます。(終了判定は違和感があるかも知れませんね。}}1-2.イテレータによるforループ同じことをイテレータを使うと次のように書くことができます。}}std::vector<int>::iteratorはstd::vector<int>用のイテレータです。end()メンバ関数が「最後の要素」ではなく「最後の要素の次」を返すことで、上記のように!=判定で容易にループを回せます。「最後の要素」を返されるとループをうまく回せません。そのため、(ちょっと分かりにくいですが)end()は「最後の要素の次」を返します。1-3.autoを使って簡単化にfor文の中身がごちゃごちゃして読みにくいですが、C++11で導入されたautoを使うとスッキリします。}}この場合、aVector.begin()が返す値はstd::vector<int>::iteratorですので、コンパイラがautoをstd::vector<int>::iteratorと解釈してくれます。従って、itはstd::vector<int>::iterator型となります。ソースを書く人はauto一発で済むので大変楽になります。ソースを読む人はaVector.begin()の戻り値の型を調べないとitの型が分からないので意外に不便です。どちらを取るかはお好みです。1-4.速度を気にする場合end()関数を毎回呼び出してますが、これが何か重たい処理をしているかも知れせん。そのような場合にこのように記述すると無駄に遅くなる可能性があります。そこで、次のように記述する場合も多いです。}}なお、1-4のような書き方をして、かつ、ループの内部処理でend()が代わるような処理(要素の追加や削除)を行うと、確実におかしくなります。1-3のように毎回end()と比較すれば大丈夫な場合も多いですが、はまりやすいです。使うコンテナはstd::vectorと思いますが、特にstd::vectorの場合、要素を追加する時に内部記憶領域を再獲得するため各要素のアドレスが変わることがあります。するとitが無効になるのです。結果、異常動作します。そもそもitが指している要素を削除すると当然itは無効になりますし、ループ内部で要素数が変化するような処理はなるべく書かない方が安全です。1-5.イテレータのメリットさて、イテレータはちょっと書き方が面倒ですね。ここまでですと面倒なだけでメリットがないように見えます。線形リスト(std::listなど)の場合、メモリ中に連続して要素が並びません。このような場合、ポインタで枚挙することはできません。ポインタをインクリメントするとポインタのアドレス値がポインタの指す型のバイト数だけ大きくなるだけですので、そこに「次の要素」が存在するとは限らないですから。}}上記のコードは最も簡単な線形リストであるstd::forward_listを用いた例です。枚挙処理はstd::vectorの時と同じ記述で済みます。本質的に同じ処理を同じ記述でかけるのは素晴らしいことです。<algorithm>に様々な汎用アルゴリズムが用意されています。これらはイテレータで処理対象を指定します。イテレータを提供するコンテナについて、そのコンテナ内部の実装と無関係にこれらのアルゴリズムを適用できるのです。イテレータは、データを枚挙して処理する多くのアルゴリズムが様々なデータ構造を処理できるようにする強力な仕組みなのです。1-5.サンプル・コードのまとめ以上のまとめをWandboxで試してみる2.イテレータの仕組みさて、iteratorクラス、ちょっと不思議ですね。本物は様々な機能を持っているので複雑ですが、本質は実は簡単です。線形リスト・コンテナの場合を例にとって説明します。最終要素の次を指すイテレータを返却するend()関数さて、以下の3つは見慣れないと思います。比較演算子(!=)C++はクラスのメンバ関数として、これらの演算子を定義することができるのです。そのメンバ関数の引数の数が決まっています。引数の型、戻り値の型、および、処理内容を自由に定義することができます。演算子のオーバーロードと呼ばれる機能です。文法的には自由に機能を定めることができますが、元の意味とあまり大きく異ならないように定義することを強くお勧めします。`+`を`-`のように定義したら他のプロジェクトの仲間が酷い目に会いますから。分解してサンプル・ソースを説明しつつ提示します。2-1-1.コンテナへ記録するクラス今回は次のような名前と年齢を記録するクラスFooを線形リスト型のコンテナへ記録します。Fooクラスには内容を表示するためにメンバ関数print()を実装してみました。}};2-1-2.簡単な線形リスト型のコンテナクラスFooを線形リストとして保管するクラスです。std::forward_listをFoo専用とし、更に重要部分だけ残して簡略化したイメージです。次の部分はそのコンテナ・クラスのコンテナ機能を担う部分です。}(後略:この後にイテレータ機能を記述します。)Fooと次のNodeへのポインタを持つNodeクラスによりFooクラスのデータを記録し、線形リストとして管理します。リスト先頭を指すmHeadポインタを使ってその線形リストの先頭要素をポイントしています。Fooインスタンスを1つだけForwardクラスで管理している時のメモリは次のようになります。push_front処理を図に示すと次のようなイメージになります。Nodeクラスをnewし、その時のmNextは直前のmHeadをコピーします。mHeadで新規にnewされたNodeクラスのインスタンスをポイントします。pop_frontはその逆を行います。これで単純な線形リストを生成・管理できます。2-1-3.簡単な線形リスト型のコンテナのイテレータ機能STLにあるstd::forward_list等の各コンテナは、その全要素を操作するためのイテレータが定義されています。上記の簡易forward_listにも同様に簡単なイテレータを定義すると次のソースを使いします。end()メンバ関数を追加しています。最後にメイン・ルーチンです。}}この処理により、Forwardクラスには次のようにデータが生成されます。「イテレータによるforループ」によるforループにより、内部で定義したiteratorクラスで全要素を枚挙しています。begin()はmHeadが指す要素へのイテレータを返却します。end()は最後の次です。最後はmName=”Taro”のNodeです。その次を指すmNextは常にnullptrですのでend()は常にnullptrを返却します。for文の;;中央でループ継続判断のために比較演算子(!=)を使います。for文の;;最後でループ変数更新処理のため、前置インクリメント演算子(++)を使っています。std::forward_listと同じくForwardもmHeadから枚挙するのでpush_frontした順序と逆の順序ででてきます。ちなみに、「1-4.イテレータのメリット」ではstd::forward_listから登録順で枚挙されているように見えますが、コンストラクタで逆順に登録されるためです。push_frontで登録すると逆順になります。1`等の演算子を使いたい場合は、該当の演算子をiteratorクラスに定義しておく必要があります。STLのイテレータはそれらの演算子の多くが事前に定義されていますので便利です。(ただし、オーバーヘッドがあるような演算子は定義されていないことがあります。Library)と呼ばれる標準ライブラリがあります。この中に多くのコンテナが定義されています。それぞれのコンテナはイテレータが定義されています。Iteratorは、コンテナの要素を枚挙するためのイテレータではないので分かりにくいと思います。std::istreamやstd::ostreamはデータの流れですが、これらは文字を枚挙することと本質的に同じです。先に述べた標準ライブラリの<algorithm>に対して、直接std::istreamやstd::ostreamのデータを流し込んだり、取り出したりできるとスマートです。Iteratorが便利です。これらのイテレータは継承関係にありますので、お互いに包含関係にあります。図にすると下記となります。なんとなくイメージを掴んで頂ければ幸いです。次の表はこれらのイテレータが定義されているSTLコンテナのリストです。前章までで説明したようにイテレータはなかなか強力なのですが、正直使い勝手は悪いと思います。継続判定がうざいです。標準のalgorithmにはbeginとendを渡しますが、間違って別のコンテナのendを渡すと上述したように!=で継続判定するため無限ループになります。メモリ破壊しまくりですね。algorithmをよくみるとイテレータは「開始」(begin)と「終了の次」(end)のペアで与えることも多いです。処理範囲を指定するのですから当然ですね。そこで、「開始」と「終了の次」のイテレータをペアにしたRange(範囲)が提案されています。Go」(イテレータなんていらない)と言う主張もあります。Go」を訳してみたで訳されてます。(先日まで【原文】と【日本語訳】はあったのですが今はなくなっしました。原文が削除されたからかも知れません。【Slideshare】は残ってます。)主旨としては「iteratorは不便なのでRangeを使おう」って感じです。Rangeの具体的なメリットがたくさん列挙されています。お手軽にコンテナの中身を枚挙できるので便利です。使い方は、概ね次のような形式が良いです。参照に「範囲」の「開始」から「終了」までの要素への参照が取り出されます。参照を使った処理}もしくは、要素のサイズが小さく、かつ、内容を更新しない時は、下記が良いです。変数に「範囲」の「開始」から「終了」までの要素がコピーして渡されます。変数を使った処理}さて、まだ「範囲」(Range)は採用されていませんので、これだけでは役に立たないように見えます。実は「範囲」(Range)が満たすべき要件は、「開始」と「終了の次」のイテレータを取り出せることです。begin()とend()でイテレータを取り出せれば良いのです。いうことは、全てのSTLのコンテナが該当します。C++の生の配列も「コンテナ」として扱えるよう設計されています。}}Wandboxで試してみる3-2.範囲ベースforの仕組み実はプリプロセッサがソースを展開してからコンパイラがコンバイルするように、コンパイラがソースをこっそり修正して修正したものをコンパイルします。参照を使った処理}と書かれていて、かつ、「範囲」がbegin()とend()を持つ場合、次のようにこっそり修正してコンパイルされます。残念ながらitやendにアクセスすることはできません。参照を使った処理}「1-4.速度を気にする場合」にそっくりですね。「範囲ベースfor用追加処理」が追加されただけです。「範囲」が生配列の場合は次のようにこっそり修正されます。(配列の要素数をNとします。&範囲[N]は最後の次の要素へのポインタです。参照を使った処理}なお、厳密には異なります。判りやすさのため重要でない相違は省いてます。statementに記載されています。end→__endです。3-2-1.線形リスト・コンテナ・クラスのサンプルを範囲ベースforに対応ということで、「2-1.線形リスト・コンテナ・クラスのサンプル」で記述した簡易版コンテナのイテレータに、operator*を追加するだけで範囲ベースforを使えるようになります。---Jiro(23)Hanako(26)Taro(30)4.まとめ今回は主にイテレータの使い方とその仕組み、および、C++11で導入された範囲ベースforの使い方と仕組みを解説しました。中身が分からないまま使うと苦労しますが、仕組みを把握すればスムーズに使えると思います。STLで定義されているものは結構複雑ですが、重要部分だけ抜き出すと意外に簡単です。今回はその簡単な部分を抽出してイテレータと範囲ベースforの考え方まで踏み込んで解説してみました。分かり易く書けていると良いのですが。今までサンプル・ソースでconstを説明しないまま使ってしまいました。本当はもっと早く解説したかったのですが、意外にconstの概念も難しいのです。constを解説するために必要な概念の説明が終わったと思います。次回constについて解説します。イライラするconst。コメントをキャンセルメールアドレスが公開されることはありません。テクノロジーの登録商標です。technology.。