この記事のポイント

  • イテレータ無効化の発生条件をレビューアー視点で体系整理
  • コンテナ種類別の無効化パターンを具体例で確認
  • 無効化が設計契約のどこに責任分離されるかをレビューで読み解く力を養う

そもそもイテレータ無効化(invalidation)とは

イテレータ無効化とは:

  • イテレータが指し示す要素・領域が
  • 何らかの操作(挿入・削除・再配置等)により
  • 存在しなくなる or 位置保証が崩れる

状態を指します。

無効化されたイテレータを使用すると:

  • 未定義動作(UB)
    → 実行時例外にならず、異常終了・暴走・情報漏洩など不定結果になる
  • C++はイテレータ安全性に強いが所有権・範囲保証はプログラマ責任
  • 無効化リスク監視は設計レビュー最大級の責任領域となる

なぜこれをレビューするのか

レビューアー視点

イテレータ無効化は以下の設計論点に直結します。

  • ライフサイクル責任の所在
    → イテレータ有効期間を誰が保証するか

  • 操作系列の順序設計
    → erase→アクセス順が整理されているか

  • 破壊操作とイテレータの距離
    → 保持側が破壊を許容していないか

  • コンテナ別無効化条件整理
    → vector/list/mapなどコンテナ特性理解が設計に反映されているか

  • API契約で無効化可能性が文書化されているか

開発者視点

  • erase直後の++で誤動作
  • resize後の全イテレータ再利用
  • insert後のイテレータ再利用
  • remove-eraseイディオム理解不足
  • list/mapをvectorと同列扱い

レビューアーはこれらの「誤解設計」を早期発見します。

コンテナ別イテレータ無効化整理表

コンテナ insert erase resize clear swap
vector 位置以降全失効 位置以降全失効 全失効 全失効 全失効
deque 挿入位置周辺失効 位置周辺失効 全失効 全失効 全失効
list 挿入影響無 当該要素のみ失効 無失効 全失効 全失効
map/set 無失効 当該要素のみ失効 無失効 全失効 全失効
unordered_map 無失効 当該要素のみ失効 無失効 全失効 全失効
  • vectorが最も無効化が激しい
  • list/map系はerase当該要素のみが失効

良い実装例

ユースケース:APIログ蓄積からエラーログだけ抽出削除

良い実装例:イテレータ失効を安全管理
#include <vector>
#include <string>
#include <cstdint>

struct ApiRequestLog {
    std::string requestId;
    int responseCode;
    int64_t requestedAt;
};

class RequestLogManager {
public:
    void add(const ApiRequestLog& log) {
        logs_.push_back(log);
    }

    void removeErrors() {
        auto it = logs_.begin();
        while (it != logs_.end()) {
            if (it->responseCode >= 500) {
                it = logs_.erase(it); // erase戻り値で更新
            } else {
                ++it;
            }
        }
    }

private:
    std::vector<ApiRequestLog> logs_;
};
  • eraseは戻り値イテレータを受ける設計が基本形
  • ++itではなくit=erase(it)で失効防止

レビュー観点

  • erase直後に返却イテレータ使用しているか
  • erase後のイテレータ++誤用を排除できているか
  • eraseとrange-based for混在を防いでいるか
  • コンテナ固有の失効条件を意識設計しているか

良くない実装例: ケース1

以下はerase直後に++itを実行し失効イテレータ利用してしまった例です。

erase++誤用例
void removeErrors() {
    for (auto it = logs_.begin(); it != logs_.end(); ++it) {
        if (it->responseCode >= 500) {
            logs_.erase(it);  // erase後に++it 実行 → UB
        }
    }
}
@Reviewer
erase後のイテレータは失効します。返却イテレータで更新してください。

問題点

  • erase実行時点でitは失効
  • ++it実行は未定義動作領域

改善例

修正例:返却イテレータ利用
for (auto it = logs_.begin(); it != logs_.end(); ) {
    if (it->responseCode >= 500) {
        it = logs_.erase(it);
    } else {
        ++it;
    }
}

良くない実装例: ケース2

次はresize後にイテレータを継続利用してしまった例です。

resize誤用例
auto it = logs_.begin();
logs_.resize(200);
*it;  // UB:resizeでイテレータ全失効
@Reviewer
resize後は全イテレータ失効します。事前にイテレータ参照保持は行わないでください。

問題点

  • resizeは全領域再配置可能
  • 旧イテレータは完全失効

改善例

修正例:resize後は新イテレータ取得
logs_.resize(200);
for (auto it = logs_.begin(); it != logs_.end(); ++it) { ... }

良くない実装例: ケース3

次はremove-eraseイディオム未適用で失効を誘発している例です。

remove誤用例
logs_.erase(std::remove_if(logs_.begin(), logs_.end(), isError), logs_.end());
@Reviewer
remove-eraseイディオムの正形ですが、この形自体がeraseのイテレータ戻り規約に依存しています。初心者はrange-forで破壊せずfilterコピー設計も検討できます。

問題点

  • 正しくはあるが、初心者レビューでは設計難易度高
  • erase失効とアルゴリズム融合箇所で事故が出やすい

改善例

修正例:破壊責務分離設計
std::vector<ApiRequestLog> filtered;
std::copy_if(logs_.begin(), logs_.end(), std::back_inserter(filtered), isValid);
logs_.swap(filtered);
  • フィルタリングは「破壊」より「新構成」設計が安全

コンテナ毎の無効化責任構造整理

vector

  • resize → 全失効
  • insert → 位置以降全失効
  • erase → 位置以降全失効

list

  • erase → 当該要素のみ失効
  • insert → 無影響

map/set

  • erase → 当該要素のみ失効
  • insert → 無影響

unordered_map

  • erase → 当該要素のみ失効
  • rehash発生時は全失効可能性あり(高負荷時特有)

PlantUMLで無効化責任整理

UML Diagram

観点チェックリスト

実務レビューFAQ

Q1. vectorのinsertは常に全失効?

→ 挿入位置より後方のイテレータのみ失効。先頭insertは全体失効になりやすい。

Q2. eraseと++it順序は必須?

→ erase返却イテレータで更新が原則。++it後eraseは禁則。

Q3. listは無効化安全?

→ erase対象のみ失効。安全性高いがerase順序設計は必要。

Q4. remove-eraseは推奨?

→ 高度利用者には推奨。初心者には明示的copy-if構成の方が可読性高。

Q5. unordered_mapは完全に安全?

→ rehashトリガがかかると全失効発生。事前reserve設計が重要。

まとめ

イテレータ無効化はC++設計レビューで最も事故が多い領域の一つです。
レビューアーは、

  • eraseと返却イテレータ更新のセット管理
  • resize発生有無のレビュー時確認
  • コンテナ別失効条件の整理確認
  • API契約に責任分離を埋め込むレビュー提案

これらを静的設計の段階で潰せる技術者になる必要があります。
レビューアーが守るべき最後の砦がinvalidation設計監視です。