この記事のポイント

  • C++のイテレータ境界チェックを設計責任として整理
  • 有効性・失効・範囲保証をレビューアーが読み解く技術を習得
  • 境界バグ温床になりやすいパターンの具体例を徹底収集

そもそもイテレータ境界チェックとは

イテレータはC++標準ライブラリの中心的な概念です。

  • 範囲の先頭と終端を指し示すだけのポインタ的抽象型
  • 境界を超えてもコンパイルエラーにならない(実行時動作は未定義動作)
for (auto it = v.begin(); it != v.end(); ++it) { ... }

境界チェックの責務とは:

  • イテレータが「定義された有効範囲内のみで使用される」設計保証のこと。

イテレータは次の2種の境界条件を内包します。

種類 内容
静的境界 begin〜endの論理範囲
動的境界 erase・insert等による失効タイミング
  • 境界超過は未定義動作(UB)を誘発する
  • バグ・クラッシュ・情報漏洩・攻撃対象になりやすい

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

レビューアー視点

イテレータは責任の所在が曖昧化しやすい構造です。レビューアーは次を確認します。

  • 範囲責任は誰が持つのか?
    → API利用側か、ライブラリ側か

  • 失効条件は明文化されているか?
    → erase後参照、insert後参照の保護は充分か

  • 境界超過を予防する構造が採用されているか?

  • 境界条件がAPI契約文書に明記されているか?

  • 例外安全性・並列実行時の保護が成立しているか?

開発者視点

  • end()イテレータそのものは安全だと思ってしまう
  • insert/erase後のイテレータ安全を軽視
  • 無条件++実行で越境
  • 無効イテレータをconst_castで延命
  • std::vector::operator[]と同列に考えてしまう

レビューアーはこうした「イテレータ設計責任忘却」を防止します。

良い実装例

ユースケース:APIログ履歴から時系列ソート済み部分抽出

良い実装例:安全なイテレータ境界設計
#include <vector>
#include <string>
#include <algorithm>
#include <cstdint>

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

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

    void prepare() {
        std::sort(logs_.begin(), logs_.end(), [](const auto& a, const auto& b) {
            return a.requestedAt < b.requestedAt;
        });
    }

    std::vector<ApiRequestLog> findRange(int64_t from, int64_t to) const {
        auto lower = std::lower_bound(logs_.begin(), logs_.end(), from, compare);
        auto upper = std::upper_bound(logs_.begin(), logs_.end(), to, compare);
        return std::vector<ApiRequestLog>(lower, upper);
    }

private:
    static bool compare(const ApiRequestLog& log, int64_t t) {
        return log.requestedAt < t;
    }

    std::vector<ApiRequestLog> logs_;
};
  • 範囲責任をクラス内部に閉じ込める設計
  • erase失効を発生させない構造
  • lower_bound/upper_bound使用時も境界条件を明確管理

レビュー観点

  • イテレータ境界責任がライブラリ内に閉じられているか
  • erase操作が外部から強要されない構造か
  • begin/endの意味が設計上明文化されているか
  • 操作順序が安全になるようAPI設計されているか

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

以下はend()イテレータを不適切にインクリメントしている典型例です。

end超過例
void dump() const {
    for (auto it = logs_.begin(); it <= logs_.end(); ++it) {
        // dump処理
    }
}
@Reviewer
end()は範囲外位置を指します。<=ではなく!=を使用してください。

問題点

  • end()イテレータは「範囲外を指す」特殊値
  • <=比較で越境し、未定義動作に陥る

改善例

修正例:!=比較の原則適用
for (auto it = logs_.begin(); it != logs_.end(); ++it) { ... }
  • イテレータ比較では!= end()を原則とする
  • <= / >= / > / < はイテレータで通常利用しない

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

次はerase後の無効イテレータを使用してしまう例です。

erase失効誤用例
auto it = logs_.begin();
while (it != logs_.end()) {
    if (shouldErase(*it)) {
        logs_.erase(it);
        ++it;  // erase後にイテレータ無効なのでUB
    } else {
        ++it;
    }
}
@Reviewer
erase後のイテレータは失効します。戻り値の新イテレータを受け取り利用してください。

問題点

  • eraseは削除後の次イテレータを返却する仕様
  • 返却値無視により失効イテレータ参照となる

改善例

修正例:eraseの返り値利用
auto it = logs_.begin();
while (it != logs_.end()) {
    if (shouldErase(*it)) {
        it = logs_.erase(it);
    } else {
        ++it;
    }
}
  • eraseは新たな有効イテレータ返却設計となっている

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

次はerase後にrange-based for文をそのまま使う危険例です。

range-for誤用例
for (auto& log : logs_) {
    if (shouldErase(log)) {
        logs_.erase(/* ??? */);
    }
}
@Reviewer
range-based forはeraseとの併用に適していません。erase用イテレータループを使用してください。

問題点

  • range-forはeraseに必要な位置情報を持たない
  • erase位置指定困難
  • 実質UBに陥るパターン

改善例

修正例:イテレータ専用ループに切替
for (auto it = logs_.begin(); it != logs_.end(); ) {
    if (shouldErase(*it)) {
        it = logs_.erase(it);
    } else {
        ++it;
    }
}

境界責任を整理するAPI設計パターン

パターン1: 境界抽象化をAPI側で持つ

std::vector<ApiRequestLog> findRange(int64_t from, int64_t to) const
  • 呼び出し側が境界意識不要
  • API内で完全に境界管理

パターン2: イテレータ型そのものをカプセル化

struct RangeResult {
    std::vector<ApiRequestLog>::const_iterator begin;
    std::vector<ApiRequestLog>::const_iterator end;
};
  • イテレータ返却するが、責務の塊として渡す
  • API契約が境界責任を明示

PlantUMLで設計責任構造整理

UML Diagram

観点チェックリスト

境界チェックの実務FAQ

Q1. erase後のイテレータ失効はvectorだけ?

→ list/map/unordered_mapでも失効条件が存在する。特にvectorは全面失効、listは当該要素のみ失効。erase直後は返却値使用が原則。

Q2. begin() == end()のケースはどう扱う?

→ 空範囲として自然動作する。必ず許容すべき。

Q3. insert後はイテレータ有効?

→ vectorでは全面失効可能性あり。list/map系は位置によっては有効保持も可能。

Q4. end()を越えた++は?

→ 完全未定義動作。レビューで即時修正指示必須。

まとめ

イテレータ境界チェックはC++レビュー最大級の設計責務領域です。
レビューアーは「++すれば進む」「eraseできる」だけでなく、

  • 境界保証の所在
  • 失効タイミングの整理
  • API契約の責務明示

これらを静かに読み取っていくスキルが重要です。
未定義動作温床の芽をレビューで潰せるレビューアーは極めて信頼されます。