この記事のポイント

  • イテレータ範囲安全の設計責任をレビューアー視点で整理
  • 境界条件整理、契約整理、安全設計のレビュー観点を体系化
  • 実務的に見逃されやすい「微細なバグの芽」を発見する訓練記事

そもそもイテレータ範囲外アクセスとは

C++のイテレータは基本的に範囲を超えたアクセスは未定義動作になります。

auto it = vec.end();
++it; // UB
*it;  // UB

範囲安全とは何か

  • begin() <= it < end()
  • これを常に保証するのが「範囲安全」
  • イテレータ境界超過はバグ源として非常に見逃されやすい
  • STLは範囲契約型APIで構成されている

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

レビューアー視点

イテレータ範囲安全は以下の設計責任整理が必要です。

  • 境界管理責任
    → begin/endの一貫性保証

  • 範囲契約遵守責任
    → STL APIで提供する範囲契約を逸脱していないか確認

  • イテレータ寿命保証責任
    → コンテナ変更による失効設計整理

  • 副作用混在抑止責任
    → erase中のイテレータ操作責任

  • スケーラビリティ保証責任
    → 件数増加時の境界管理破綻回避

開発者視点

  • size()を直接for条件に使う
  • end()越えを静的保証できないコードを書く
  • erase後のイテレータ無効化を軽視
  • insert中の並行参照で境界崩壊
  • API契約に境界条件を盛り込まない

レビューアーはこれら「境界責任軽視文化」をレビュー段階で是正します。

良い実装例(範囲for利用)

ユースケース:APIレスポンス全件処理

良い実装例:範囲forで境界責任転嫁
#include <vector>
#include <string>

struct ApiRequestLog {
    std::string requestId;
};

void process(const std::vector<ApiRequestLog>& logs) {
    for (const auto& log : logs) {
        handle(log);
    }
}
  • 範囲for文は範囲安全設計文化の代表
  • イテレータ境界責任が完全内部化

良い実装例(アルゴリズム使用)

良い実装例:count_ifで範囲契約内包
int countFailures(const std::vector<ApiRequestLog>& logs) {
    return std::count_if(logs.begin(), logs.end(),
        [](const ApiRequestLog& log) {
            return log.responseCode >= 500;
        });
}
  • アルゴリズム利用は範囲契約化最大の武器

レビュー観点

  • begin/endセットの整合性維持されているか
  • 明示indexアクセスでsize()-1越境が起きていないか
  • erase後のイテレータ失効責任整理ができているか
  • API契約内に範囲条件を整理できているか

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

以下はindex比較で範囲超過が容易に生まれる例。

index誤用例
for (size_t i = 0; i <= logs.size(); ++i) {  // <= になってる
    handle(logs[i]);
}
@Reviewer
size()は「要素数」。indexはsize()未満まで限定しないと越境します。

問題点

  • i == size()時に未定義動作
  • 境界条件設計崩壊

改善例

修正例:< に修正
for (size_t i = 0; i < logs.size(); ++i) {
    handle(logs[i]);
}

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

次はerase後イテレータ再利用で無効化される例。

erase失効例
for (auto it = logs.begin(); it != logs.end(); ++it) {
    if (pred(*it)) {
        logs.erase(it);
    }
}
@Reviewer
erase後はイテレータ失効します。戻り値it再代入が必要です。

問題点

  • eraseが次要素失効を誘発
  • 破壊的副作用混在

改善例

修正例:erase戻り値再利用
for (auto it = logs.begin(); it != logs.end(); ) {
    if (pred(*it)) {
        it = logs.erase(it);
    } else {
        ++it;
    }
}

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

次はerase-removeイディオム適用忘れによる境界崩壊例。

erase忘却例
std::remove_if(logs.begin(), logs.end(), pred);
@Reviewer
remove_if後はerase統合しないと未削除範囲が残ります。

問題点

  • erase統合責任が放棄される
  • size()との整合性崩壊

改善例

修正例:remove-eraseイディオム適用
logs.erase(std::remove_if(logs.begin(), logs.end(), pred), logs.end());

API契約整理パターン

パターンA:イテレータ引数API

void processLogs(std::vector<ApiRequestLog>::iterator first,
                 std::vector<ApiRequestLog>::iterator last);
  • 呼出側が範囲管理責任保持
  • 汎用API向き

パターンB:コレクション引数API

void processLogs(const std::vector<ApiRequestLog>& logs);
  • API側が境界保証責任保持
  • 安全性高

範囲安全戦略早見表

処理形態 推奨戦略
単純全件走査 範囲for
条件件数計測 count_if
条件抽出 copy_if
条件削除 remove_if+erase
  • アルゴリズム文化は範囲安全文化

PlantUMLで設計責任整理

UML Diagram

観点チェックリスト

実務レビューFAQ

Q1. indexループは必ず悪?
→ 明示目的なら容認。ただし境界誤差をレビューで常に監視。

Q2. erase後のイテレータ失効は規格上保証?
→ erase戻り値使用が標準設計文化。失効確認はレビュー必須。

Q3. remove_ifは削除処理?
→ 前詰めのみ。erase統合必須。

Q4. アルゴリズム使用は過剰抽象?
→ 逆。範囲安全の設計文化基盤。

Q5. API契約で範囲管理委譲はいつ妥当?
→ 汎用API時はiterator引数。業務API時はコレクション受領が安全。

まとめ

イテレータ範囲安全レビューは実務品質レビューの基本技術である。
レビューアーは

  • 境界責任整理
  • erase副作用整理
  • remove-erase統合責任整理
  • API契約昇格整理

を読み解き、「範囲管理は設計者の責任」を文化化するレビュー技術を育成する必要がある。
レビューアーが境界責任整理を体系化できると設計品質は非常に安定する。