C++レビュー|イテレータ境界チェック設計責任とレビュー観点の徹底整理
この記事のポイント
- 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()イテレータを不適切にインクリメントしている典型例です。
void dump() const {
for (auto it = logs_.begin(); it <= logs_.end(); ++it) {
// dump処理
}
}
@Reviewerend()は範囲外位置を指します。<=ではなく!=を使用してください。
問題点
- end()イテレータは「範囲外を指す」特殊値
- <=比較で越境し、未定義動作に陥る
改善例
for (auto it = logs_.begin(); it != logs_.end(); ++it) { ... }- イテレータ比較では
!= end()を原則とする - <= / >= / > / < はイテレータで通常利用しない
良くない実装例: ケース2
次はerase後の無効イテレータを使用してしまう例です。
auto it = logs_.begin();
while (it != logs_.end()) {
if (shouldErase(*it)) {
logs_.erase(it);
++it; // erase後にイテレータ無効なのでUB
} else {
++it;
}
}
@Reviewererase後のイテレータは失効します。戻り値の新イテレータを受け取り利用してください。
問題点
- eraseは削除後の次イテレータを返却する仕様
- 返却値無視により失効イテレータ参照となる
改善例
auto it = logs_.begin();
while (it != logs_.end()) {
if (shouldErase(*it)) {
it = logs_.erase(it);
} else {
++it;
}
}- eraseは新たな有効イテレータ返却設計となっている
良くない実装例: ケース3
次はerase後にrange-based for文をそのまま使う危険例です。
for (auto& log : logs_) {
if (shouldErase(log)) {
logs_.erase(/* ??? */);
}
}
@Reviewerrange-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で設計責任構造整理
観点チェックリスト
境界チェックの実務FAQ
Q1. erase後のイテレータ失効はvectorだけ?
→ list/map/unordered_mapでも失効条件が存在する。特にvectorは全面失効、listは当該要素のみ失効。erase直後は返却値使用が原則。
Q2. begin() == end()のケースはどう扱う?
→ 空範囲として自然動作する。必ず許容すべき。
Q3. insert後はイテレータ有効?
→ vectorでは全面失効可能性あり。list/map系は位置によっては有効保持も可能。
Q4. end()を越えた++は?
→ 完全未定義動作。レビューで即時修正指示必須。
まとめ
イテレータ境界チェックはC++レビュー最大級の設計責務領域です。
レビューアーは「++すれば進む」「eraseできる」だけでなく、
- 境界保証の所在
- 失効タイミングの整理
- API契約の責務明示
これらを静かに読み取っていくスキルが重要です。
未定義動作温床の芽をレビューで潰せるレビューアーは極めて信頼されます。
