C++
 Computer >> コンピューター >  >> プログラミング >> C++

C++におけるイテレータの無効化――原因とコンテナ別のルールを解説

イテレータ無効化とは

C++には、vector、list、set、mapなど、用途に応じたさまざまなコンテナが標準ライブラリとして用意されています。これらのコンテナを走査する際にはイテレータを使用しますが、扱い方を誤ると予期しない動作を引き起こすことがあります。特に注意が必要なのが「イテレータの無効化(invalidation)」です。コンテナを反復処理している最中に要素の追加や削除などでコンテナの構造やサイズが変化すると、イテレータが無効化され、最悪の場合は未定義動作につながります。

以下のサンプルコードで、無効化が引き起こす問題を確認してみましょう。

問題を引き起こすサンプルコード

#include <iostream>
#include <vector>
using namespace std;
int main() {
    vector <int> vec{11, 55, 110, 155, 220};
    for (auto it=vec.begin(); it!=vec.end(); it++)
        if ((*it) == 110)
            vec.push_back(89); // 反復処理中に新しい値を挿入
    for (auto it=vec.begin(); it!=vec.end(); it++)
        cout << (*it) << " ";
}

実行結果

11 55 110 155 220 89 89

なぜ無効化が発生するのか

このプログラムでは、実行環境によって異なる結果が得られる可能性があります。vectorのサイズは事前に確保されておらず、初期値から自動的に決定されます。反復処理中にpush_back()で新しい値を追加すると、vectorに十分な空き容量(capacity)がない場合、実行時に新たなメモリブロックが確保され、既存の全要素がその領域へコピーされます。しかし、それまで使用していたイテレータは旧メモリ領域のアドレスを指したままになるため、無効化された状態となります。無効化されたイテレータを使い続けると、プログラムは不正な動作をする恐れがあります。

コンテナ別のイテレータ無効化ルール

イテレータが無効化される条件は、コンテナの種類や操作内容によって異なります。主なルールを以下の表にまとめました。

 挿入(Insertion)削除(Erasure)リサイズ(Resizing)
vector挿入位置より前の要素を指すイテレータは影響を受けませんが、挿入位置以降を指すイテレータは無効化されます。また、容量の再割り当てが発生した場合は、すべてのイテレータが無効化されます。削除位置より後ろの要素を指すイテレータおよび参照は、すべて無効化されます。挿入・削除の場合と同じ挙動になります。
dequedequeの末尾以外の位置に要素を挿入した場合、すべてのイテレータと参照が無効化されます。末尾以外の位置から要素を削除した場合、すべてのイテレータが無効化されます。挿入・削除の場合と同じ挙動になります。
listすべてのイテレータと参照は影響を受けません。削除される要素そのものを指しているイテレータおよび参照のみが影響を受けます。挿入・削除の場合と同じ挙動になります。
set / map / multiset / multimapすべてのイテレータと参照は影響を受けません。削除される要素そのものを指しているイテレータおよび参照のみが影響を受けます。----

無効化を防ぐためのポイント

  • vectorでは、あらかじめreserve()で必要な容量を確保しておくと、再割り当てによる無効化を回避できます。
  • 要素を削除する際は、erase()の戻り値(削除要素の次の要素を指す有効なイテレータ)を受け取ってイテレータを更新しましょう。
  • 反復処理中のコンテナ変更は避け、変更処理と走査処理を分けるのが安全です。

イテレータの無効化は、コンパイルエラーにならず実行時に問題が顕在化するため発見が難しいバグの一つです。コンテナの特性を理解し、安全なコードを心がけましょう。


  1. C++で実装する二分探索木(BST)イテレータの作り方

    二分探索木(BST)に対するイテレータを実装することを考えてみましょう。このイテレータには、次の2つのメソッドが必要です。 next():次の要素(次に小さい値)を返すメソッド hasNext():次の要素が存在するかどうかをブール値で返すメソッド 例えば、以下のような二分探索木があるとします。 この木に対して、関数呼び出しのシーケンスが [next(), next(), hasNext(), next(), hasNext(), next(), hasNext(), next(), hasNext()] である場合、出力は [3, 7, true, 9, true, 15, true,

  2. C#のイテレータとは?yield returnによる反復処理の基本を解説

    イテレータとは イテレータ(Iterator)は、コレクションに対して独自の反復処理(イテレーション)を実行するための仕組みです。C#では、yield returnステートメントを使用することで、コレクションの各要素を1つずつ順番に返すことができます。 イテレータは現在の実行位置を記憶しています。そのため、次に反復処理が要求されたときには、前回の続きから次の要素が返されます。この特性により、大量のデータを扱う場合でもメモリ効率の良い処理が可能になります。 サンプルコード 以下は、C#でイテレータを使用する例です。 using System; using System.Collections.G