web-dev-qa-db-ja.com

イテレータループとインデックスループ

可能性のある複製:
配列インデックスの代わりにイテレータを使用する理由

私はC++の知識をレビューしていますが、イテレーターにつまずきました。私が知りたいことの1つは、それらを特別なものにしている理由です。

using namespace std;

vector<int> myIntVector;
vector<int>::iterator myIntVectorIterator;

// Add some elements to myIntVector
myIntVector.Push_back(1);
myIntVector.Push_back(4);
myIntVector.Push_back(8);

for(myIntVectorIterator = myIntVector.begin(); 
        myIntVectorIterator != myIntVector.end();
        myIntVectorIterator++)
{
    cout<<*myIntVectorIterator<<" ";
    //Should output 1 4 8
}

これよりも優れています:

using namespace std;

vector<int> myIntVector;
// Add some elements to myIntVector
myIntVector.Push_back(1);
myIntVector.Push_back(4);
myIntVector.Push_back(8);

for(int y=0; y<myIntVector.size(); y++)
{
    cout<<myIntVector[y]<<" ";
    //Should output 1 4 8
}

そして、はい、私はstd名前空間を使うべきではないことを知っています。私はcprogramming Webサイトからこの例を取り上げました。後者の方が悪い理由を教えてください。大きな違いは何ですか?

94
CodingMadeEasy

イテレータの特別な点は、 アルゴリズムとコンテナ の間に接着剤を提供することです。汎用コードの場合、推奨事項は、STLアルゴリズム(findsortremovecopyなど)の組み合わせを使用することです。データ構造(vectorlistmapなど)を念頭に置き、イテレーターを使用してそのアルゴリズムをコンテナーに提供します。

特定の例は、for_eachアルゴリズムとvectorコンテナー(以下のオプション3を参照)の組み合わせとして記述できますが、std :: vectorを反復処理する4つの異なる方法のうちの1つにすぎません。

1)インデックスベースの反復

for (std::size_t i = 0; i != v.size(); ++i) {
    // access element as v[i]

    // any code including continue, break, return
}

利点:Cスタイルのコードに精通している人なら誰でも知っている、異なるストライドを使用してループすることができます(例えばi += 2)。

欠点:シーケンシャルランダムアクセスコンテナ(vectorarraydeque)のみ、listには機能しません、forward_listまたは連想コンテナ。また、ループ制御は少し冗長です(init、check、increment)。 C++の0ベースのインデックス付けを知っておく必要があります。

2)反復子ベースの反復

for (auto it = v.begin(); it != v.end(); ++it) {
    // if the current index is needed:
    auto i = std::distance(v.begin(), it); 

    // access element as *it

    // any code including continue, break, return
}

利点:より汎用的で、すべてのコンテナで機能します(新しい順序付けられていない連想コンテナでも、異なるストライドを使用できます(例:std::advance(it, 2));

欠点:現在の要素のインデックスを取得するために余分な作業が必要です(listまたはforward_listの場合はO(N)である可能性があります)。繰り返しますが、ループ制御は少し冗長です(init、check、increment)。

3)STL for_eachアルゴリズム+ lambda

std::for_each(v.begin(), v.end(), [](T const& elem) {
     // if the current index is needed:
     auto i = &elem - &v[0];

     // cannot continue, break or return out of the loop
});

利点:2と同じ)に加えて、ループ制御のわずかな削減(チェックおよびインクリメントなし)、これによりバグ率を大幅に削減できます(不正なinit、checkまたはincrement、off-by- 1つのエラー)。

短所:明示的なイテレータループに加えて、ループ内のフロー制御の制限された可能性(continue、break、returnを使用できない)、および異なるストライドのオプションなし(イテレータを使用しない限り) operator++をオーバーロードするアダプター。

4)range-forループ

for (auto& elem: v) {
     // if the current index is needed:
     auto i = &elem - &v[0];

    // any code including continue, break, return
}

利点:非常にコンパクトなループ制御、現在の要素への直接アクセス。

欠点:インデックスを取得するための追加ステートメント。異なるストライドを使用することはできません。

何を使用しますか?

std::vectorを反復処理する特定の例:本当にインデックスが必要な場合(たとえば、前または次の要素にアクセスする、ループ内でインデックスを印刷/記録するなど)、または1以外のストライドが必要な場合は、明示的にインデックス付きループ、そうでなければrange-forループに行きます。

ジェネリックコンテナーのジェネリックアルゴリズムの場合、コード内にフロー制御が含まれておらず、ストライド1が必要な場合を除き、明示的なイテレーターループを使用します。その場合、STL for_each +ラムダを使用します。

160
TemplateRex

イテレータはコードをより一般的にします。
すべての標準ライブラリコンテナはイテレータを提供するため、将来コンテナクラスを変更しても、ループには影響しません。

9
Alok Save

イテレータはoperator[]よりも最初に選択されます。 C++ 11はstd::begin()std::end()関数を提供します。

コードはstd::vectorのみを使用しているため、両方のコードに大きな違いがあるとは言えませんが、operator []は意図したとおりに動作しない可能性があります。たとえば、マップを使用する場合、operator[]は、見つからない場合は要素を挿入します。

また、iteratorを使用することで、コンテナ間でコードの移植性が高まります。コンテナをstd::vectorからstd::listまたは他のコンテナに自由に切り替えることができます。イテレータを使用すると、このようなルールはoperator[]に適用されません。

7
billz

ベクトル反復子では、実際の利点はありません。構文は見栄えが悪く、入力するのが長くなり、読みにくくなります。

反復子を使用したベクトルの反復は高速でも安全でもありません(実際、反復子を使用した反復中にベクトルのサイズを変更すると、大きな問題が発生します)。

後でコンテナタイプを変更するときに機能する汎用ループを持つという考え方も、実際のケースではほとんど意味がありません。残念ながら、深刻な型付け推論のない厳密に型付けされた言語の暗黒面(ただし、C++ 11では少し改善されています)は、各ステップですべての型が何であるかを言う必要があることです。後で気が変わった場合でも、すべてを変更する必要があります。さらに、コンテナごとにトレードオフが非常に異なり、コンテナの種類を変更することはそれほど頻繁には発生しません。

可能な場合、反復を保持する必要がある唯一のケースは、テンプレートコードを書くときですが、それは(あなたに期待しています)最も頻繁なケースではありません。

明示的なインデックスループに存在する唯一の問題は、sizeが符号なしの値(C++の設計バグ)を返すことであり、符号付きと符号なしの比較は危険で驚くべきことなので、避けるべきです。警告を有効にしてまともなコンパイラを使用する場合、その診断が必要です。

符号なしの値間の算術も明らかに論理的ではないため(モジュロ算術であり、x-1xよりも大きい可能性があるため)、解決策はインデックスとしてunsigedを使用しないことに注意してください。代わりに、使用する前にサイズを整数にキャストする必要があります。それはmay16で作業している場合にのみ、符号なしのサイズとインデックス(書くすべての式に多くの注意を払う)を使用する意味がありますビットC++実装( サイズに符号なしの値がある理由は16ビット )。

符号なしサイズが導入する典型的な間違いとして、以下を考慮してください。

void drawPolyline(const std::vector<P2d>& points)
{
    for (int i=0; i<points.size()-1; i++)
        drawLine(points[i], points[i+1]);
}

空のpointsベクトルを渡すと、値points.size()-1が非常に正の数になり、セグメンテーションフォールトにループするため、バグが存在します。実用的なソリューションは

for (int i=1; i<points.size(); i++)
    drawLine(points[i - 1], points[i]);

しかし、個人的には、unsinged- nessを常にint(v.size())で削除することを好みます。

この場合のイテレーター使用のさは、読者の課題として残されています。

5
6502

常に必要なものに依存します。

必要ベクトル内の要素に直接アクセスする場合(ベクトル内の特定の要素にインデックスを付ける必要がある場合)は、operator[]を使用する必要があります。イテレータで使用しても何も問題はありません。ただし、ニーズ(operator[]または反復子)に最も適しているものを自分で決定する必要があります。

イテレータを使用すると、コードを大幅に変更することなく、他のコンテナタイプに切り替えることができます。つまり、イテレータを使用するとコードがより汎用的になり、特定の種類のコンテナに依存しません。

4
Mark Garcia

反復子の観点からクライアントコードを記述することにより、コンテナを完全に抽象化します。

次のコードを検討してください。

class ExpressionParser // some generic arbitrary expression parser
{
public:
    template<typename It>
    void parse(It begin, const It end)
    {
        using namespace std;
        using namespace std::placeholders;
        for_each(begin, end, 
            bind(&ExpressionParser::process_next, this, _1);
    }
    // process next char in a stream (defined elsewhere)
    void process_next(char c);
};

クライアントコード:

ExpressionParser p;

std::string expression("SUM(A) FOR A in [1, 2, 3, 4]");
p.parse(expression.begin(), expression.end());

std::istringstream file("expression.txt");
p.parse(std::istringstream<char>(file), std::istringstream<char>());

char expr[] = "[12a^2 + 13a - 5] with a=108";
p.parse(std::begin(expr), std::end(expr));

編集:元のコード例を検討してください:

using namespace std;

vector<int> myIntVector;
// Add some elements to myIntVector
myIntVector.Push_back(1);
myIntVector.Push_back(4);
myIntVector.Push_back(8);

copy(myIntVector.begin(), myIntVector.end(), 
    std::ostream_iterator<int>(cout, " "));
1
utnapistim

イテレーターの良いところは、後でベクターを別のSTDコンテナーに切り替えたい場合です。その後、forloopは引き続き機能します。

0
Caesar