138

重複の可能性:
配列インデックスの代わりに反復子を使用するのはなぜですか?

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 サイトから取り出しました。後者のほうが悪い理由を教えてください。大きな違いは何ですか?

4

8 に答える 8

220

イテレータの特別な点は、アルゴリズムとコンテナの間の接着剤を提供することです。一般的なコードの場合、データ構造 ( 、、など)で念頭に置いている計算を実行するSTL アルゴリズム ( findsortremoveなど) などの組み合わせを使用し、そのアルゴリズムにイテレータをコンテナに追加します。copyvectorlistmap

特定の例は、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)。

短所: シーケンシャル ランダム アクセス コンテナー ( vectorarray、 ) の場合のみ、 、または連想コンテナーdequeでは機能しません。また、ループ制御は少し冗長です (init、check、increment)。C++ の 0 ベースのインデックス付けに注意する必要があります。listforward_list

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 アルゴリズム + ラムダ

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) と同じに加えて、ループ制御 (チェックとインクリメントなし) のわずかな削減により、バグ率 (間違った初期化、チェックまたはインクリメント、off-by-one エラー) を大幅に削減できます。

欠点: 明示的なイテレータ ループと同じで、さらにループ内のフロー制御の可能性が制限され (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+ ラムダを使用します。

于 2013-01-17T08:04:27.000 に答える
10

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

イテレータを使用してベクトルを反復することは、高速ではなく安全でもありません (実際、イテレータを使用して反復中にベクトルのサイズが変更される可能性がある場合は、大きな問題が発生します)。

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

可能な限り一般的な反復を維持する必要がある唯一のケースは、テンプレート コードを記述する場合ですが、それは (あなたのために願っています) 最も頻繁なケースではありません。

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

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

unsigned size が導入する可能性のある典型的な間違いとして、次のことを考慮してください。

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が非常に大きな正の数になり、segfault にループするためです。実用的な解決策は

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

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

PS: もしあなたが本当に自分でその意味を考えたくないだけで、専門家に教えてもらいたいだけなら、世界的に認められたかなりの数の C++ 専門家が、以下を除いて符号なし値は悪い考えであることに同意し、意見を表明したことを考慮してください。ビット操作

最後から 2 番目まで反復する場合に反復子を使用することの醜さを発見することは、読者の演習として残されています。

于 2013-01-17T07:35:39.943 に答える
9

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

于 2013-01-17T07:18:47.637 に答える
7

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

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

また、iteratorコードを使用することで、コンテナー間での移植性が向上します。iterator を使えば、からstd::vectorへ、または他のコンテナを自由に切り替えることができます。std::listoperator[]

于 2013-01-17T07:31:32.653 に答える
4

それは常にあなたが必要とするものに依存します。

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

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

于 2013-01-17T07:17:50.543 に答える
1

クライアント コードをイテレータで記述することにより、コンテナを完全に抽象化します。

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

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, " "));
于 2013-01-17T09:36:01.197 に答える
0

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

于 2013-01-17T07:18:41.387 に答える
-1

それは速度の問題です。イテレータを使用すると、要素にすばやくアクセスできます。同様の質問がここで回答されました:

vector::iterator または at() を使用して STL ベクトルを反復する場合、どちらが高速ですか?

編集:アクセス速度はCPUとコンパイラごとに異なります

于 2013-01-17T07:18:26.673 に答える