C++レビュー|イテレータ無効化(invalidation)リスク監視と設計レビュー責任整理
この記事のポイント
- イテレータ無効化の発生条件をレビューアー視点で体系整理
- コンテナ種類別の無効化パターンを具体例で確認
- 無効化が設計契約のどこに責任分離されるかをレビューで読み解く力を養う
そもそもイテレータ無効化(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を実行し失効イテレータ利用してしまった例です。
void removeErrors() {
for (auto it = logs_.begin(); it != logs_.end(); ++it) {
if (it->responseCode >= 500) {
logs_.erase(it); // erase後に++it 実行 → UB
}
}
}
@Reviewererase後のイテレータは失効します。返却イテレータで更新してください。
問題点
- erase実行時点でitは失効
- ++it実行は未定義動作領域
改善例
for (auto it = logs_.begin(); it != logs_.end(); ) {
if (it->responseCode >= 500) {
it = logs_.erase(it);
} else {
++it;
}
}良くない実装例: ケース2
次はresize後にイテレータを継続利用してしまった例です。
auto it = logs_.begin();
logs_.resize(200);
*it; // UB:resizeでイテレータ全失効
@Reviewerresize後は全イテレータ失効します。事前にイテレータ参照保持は行わないでください。
問題点
- resizeは全領域再配置可能
- 旧イテレータは完全失効
改善例
logs_.resize(200);
for (auto it = logs_.begin(); it != logs_.end(); ++it) { ... }良くない実装例: ケース3
次はremove-eraseイディオム未適用で失効を誘発している例です。
logs_.erase(std::remove_if(logs_.begin(), logs_.end(), isError), logs_.end());
@Reviewerremove-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で無効化責任整理
観点チェックリスト
実務レビュー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設計監視です。
