この記事のポイント

  • range-based for文の設計意図と安全性をレビューアー視点で整理
  • 使ってよいケース・避けるべきケースを具体的に分類
  • イテレータ責務と所有権契約の整理ポイントを学ぶ

そもそもrange-based forとは

C++11から導入されたrange-based for文は、イテレータを直接操作せずに範囲走査を簡潔に記述できる構文です。

for (const auto& item : container) {
    // itemを利用
}

range-based forの内部動作

  • begin()end()からイテレータ範囲を取得
  • !=比較、++*参照を暗黙的に展開
  • range-based forは記法の糖衣構文であり、実行性能は通常のイテレータループと同等
  • 記法簡素化により所有権責任や境界責任が曖昧化しにくいという設計上の利点がある

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

レビューアー視点

range-based forは一見単純だが、以下の設計責任分離の明文化が問われます。

  • イテレータ失効リスクの所在確認
    → eraseなどの破壊操作との併用有無

  • コピー/参照の選択責任
    → 値コピー・const参照・ムーブの適切選択

  • 所有権契約の伝播有無
    → API間でデータが流れる範囲の整理

  • コンテナ型に依存する失効危険性
    → vector/list/mapなど型固有の内部失効整理

  • API契約文書に責任分離が反映されているか

開発者視点

  • つい毎回autoで安易に記述
  • コピーコスト無自覚
  • 削除対象含む範囲で使用
  • move適用を忘れる
  • 型制約や制限を理解せずに汎用記述

レビューアーはこれらの「糖衣構文依存思考」を抑制し、設計責任意識を育成する役割を持ちます。

良い実装例

ユースケース:APIログの単純読み取り出力

良い実装例:読み取り用途でのrange-based for活用
#include <vector>
#include <string>
#include <cstdint>

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

class RequestLogger {
public:
    void dumpLogs(const std::vector<ApiRequestLog>& logs) const {
        for (const auto& log : logs) {
            printLog(log);
        }
    }

private:
    void printLog(const ApiRequestLog& log) const {
        // 実際のログ出力処理
    }
};
  • 読み取り専用 → const参照指定でコピー不要
  • erase等の破壊操作と無縁 → 安定安全
  • API契約が読み取り専用責任を明示

レビュー観点

  • 読み取り専用設計になっているか
  • 参照型(const&)適切に選択されているか
  • erase等の破壊系操作は別途分離されているか
  • move対象ではないことが明確か
  • API契約に破壊責任の所在が整理されているか

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

以下は値コピーによる無駄な性能コストが発生している例です。

値コピー誤用例
void dumpLogs(const std::vector<ApiRequestLog>& logs) const {
    for (auto log : logs) {  // 値コピー発生
        printLog(log);
    }
}
@Reviewer
不要な値コピーが発生しています。const参照で受け取るよう修正してください。

問題点

  • 毎回ApiRequestLog全体がコピーされる
  • 無駄なメモリ確保とデストラクタ呼出し発生

改善例

修正例:const参照活用
for (const auto& log : logs) { ... }
  • 値型が重い場合は常に参照を原則に
  • 明示的にコピー必要ならその意図も設計責務化

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

次はeraseとrange-based forを安易に混在してしまった例です。

erase併用誤用例
void filterLogs(std::vector<ApiRequestLog>& logs) {
    for (auto& log : logs) {
        if (log.responseCode == 500) {
            logs.erase(/* ??? */);
        }
    }
}
@Reviewer
eraseはイテレータ無効化を伴います。range-based forとは併用せずイテレータループへ切り替えてください。

問題点

  • erase操作に位置情報が提供できない
  • range-for中にerase自体が未定義動作になり得る

改善例

修正例:イテレータループ切替
for (auto it = logs.begin(); it != logs.end(); ) {
    if (it->responseCode == 500) {
        it = logs.erase(it);
    } else {
        ++it;
    }
}
  • 破壊操作は必ずイテレータループで管理
  • range-based forは読み取り専用用法に限定

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

次は移動所有権を伴う設計でコピーが無自覚に発生しているケースです。

move対象誤用例
void process(std::vector<ApiRequestLog>&& logs) {
    for (auto log : logs) { 
        consume(std::move(log));
    }
}
@Reviewer
move対象でもrange-based for内のlogはコピー生成されます。move_iteratorを検討してください。

問題点

  • moveセマンティクスを誤認
  • range-based forは常にコピー構築を介在

改善例

修正例:move_iterator適用
for (auto&& log : logs) {
    consume(std::move(log));
}
  • 完全右辺値受取はauto&&利用で安全
  • move_iterator活用はさらに進んだ最適化

範囲保証のAPI設計パターン

パターンA: 完全読み取り用途

void dump(const std::vector<ApiRequestLog>& logs);
  • const参照で完全安全化
  • 呼び出し側の所有権保持前提

パターンB: 破壊責務分離用途

void filterInPlace(std::vector<ApiRequestLog>& logs);
  • range-for不使用を強制設計
  • イテレータ利用をレビューポリシーに組込む

PlantUMLで責務整理

UML Diagram

観点チェックリスト

実務レビューFAQ

Q1. vectorでもlistでもeraseはrange-forと併用禁止?

→ 基本禁止。eraseは位置指定必須。コンテナ種に関わらずイテレータ設計へ切替。

Q2. コピー型でautoだけ書けばいいのでは?

→ auto指定でもコピー構築が発生する。値型はconst auto&原則。

Q3. mapのrange-forも同じ?

→ mapではfor (const auto& [key, value] : map)構文が有効。ただしerase併用時は危険。

Q4. move所有権移譲の範囲-forは?

auto&&記述で完全右辺値受取に対応可能。

まとめ

range-based forはC++設計において安全な糖衣構文として非常に強力です。
レビューアーは表面的なコード簡潔性だけでなく、

  • 所有権責任
  • 破壊操作責任
  • コピーコスト
  • API契約の設計整理

これらを読み解く役割を担います。
「安全に短く書ける」=「設計が成熟している証」という視点でレビューを行いましょう。