この記事のポイント

  • copy_if, remove_ifの設計責任をレビューアー視点で整理
  • 破壊型/非破壊型フィルタリングの責任分離を読み解く
  • API契約・状態管理・副作用責任のレビュー観点を体系化

そもそもcopy_if, remove_ifとは

C++標準ライブラリalgorithmの中でも、特に要素選別責任を分離する代表がこれです。

remove_if

  • 条件に一致する要素を「前詰め」る
  • 実際の削除は行わない(eraseと組み合わせ)
auto it = std::remove_if(vec.begin(), vec.end(), pred);
vec.erase(it, vec.end());

copy_if

  • 条件に一致する要素のみを他コンテナにコピー
  • 破壊せず抽出
std::copy_if(source.begin(), source.end(), target.begin(), pred);
  • remove_if:破壊前処理
  • copy_if:破壊せず抽出処理

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

レビューアー視点

copy_if / remove_if適用設計には以下の責任整理が必要。

  • 破壊責任と保持責任の分離
    → API契約でどちらが保持管理を担当するか明確に整理

  • フィルタロジックの安定化責任
    → predicateの設計妥当性確認

  • 副作用排除責任
    → イテレータ無効化/副作用埋没を未然防止

  • スケーラビリティ保証責任
    → 件数規模に応じたcopy/remove適用妥当性確認

  • API契約明文化責任
    → 破壊系/抽出系の契約分離確認

開発者視点

  • remove_ifとeraseを分離実装し忘れる
  • copy_if適用場面を見落とす
  • predicateを状態依存にしてしまう
  • 大規模破壊と大規模コピーの負荷を誤認
  • 破壊責任と契約責任の切り分け意識が希薄

レビューアーはこれら「責任混濁設計」をレビュー時に発見する技術を鍛えます。

良い実装例(remove_if)

ユースケース:サーバーエラーのみ削除

良い実装例:remove_if+erase活用
#include <vector>
#include <algorithm>

struct ApiRequestLog {
    int responseCode;
};

void removeServerErrors(std::vector<ApiRequestLog>& logs) {
    auto it = std::remove_if(logs.begin(), logs.end(),
        [](const ApiRequestLog& log) {
            return log.responseCode >= 500;
        });
    logs.erase(it, logs.end());
}
  • 破壊系APIとして契約整理
  • remove_if→eraseの正統構成
  • 副作用ゼロ

良い実装例(copy_if)

ユースケース:成功レスポンスのみ抽出

良い実装例:copy_if活用
std::vector<ApiRequestLog> extractSuccess(const std::vector<ApiRequestLog>& logs) {
    std::vector<ApiRequestLog> filtered;
    std::copy_if(logs.begin(), logs.end(), std::back_inserter(filtered),
        [](const ApiRequestLog& log) {
            return log.responseCode >= 200 && log.responseCode < 300;
        });
    return filtered;
}
  • 非破壊系API契約
  • フィルタリング専用責任整理
  • APIが状態維持設計

レビュー観点

  • 破壊系APIと抽出系APIが分離設計されているか
  • predicate責任が状態依存していないか
  • 副作用がラムダ内に埋没していないか
  • イテレータ失効管理が責任内包されているか

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

以下はremove_if後にerase統合を忘却している例。

erase忘却例
std::remove_if(logs.begin(), logs.end(), pred);
@Reviewer
remove_ifは前詰め操作のみです。erase統合で領域削除を責任保持してください。

問題点

  • コンテナ末尾にゴミ要素残留
  • size()が期待値と不一致

改善例

修正例:erase統合
logs.erase(std::remove_if(logs.begin(), logs.end(), pred), logs.end());

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

次はcopy_if適用場面で自前ループを維持してしまった例。

copy_if見落とし例
std::vector<ApiRequestLog> filtered;
for (const auto& log : logs) {
    if (pred(log)) {
        filtered.push_back(log);
    }
}
@Reviewer
copy_if適用でフィルタ責任明文化が可能です。

問題点

  • 意図が読みにくい
  • predicate責任が埋没

改善例

修正例:copy_if適用
std::copy_if(...);

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

次はラムダ内で副作用を埋め込んでしまった例。

副作用埋没例
int counter = 0;
auto it = std::remove_if(logs.begin(), logs.end(),
    [&counter](const ApiRequestLog& log) {
        if (log.responseCode >= 500) {
            counter++;
            return true;
        }
        return false;
    });
logs.erase(it, logs.end());
@Reviewer
remove_ifは副作用排除が原則です。件数集計は事前集計責任として分離設計してください。

問題点

  • 状態依存副作用設計
  • ラムダ純粋性崩壊

改善例

修正例:副作用分離
int count = std::count_if(logs.begin(), logs.end(),
    [](const ApiRequestLog& log) {
        return log.responseCode >= 500;
    });

auto it = std::remove_if(...);
logs.erase(it, logs.end());

API契約整理パターン

パターンA:破壊系API契約

void removeInvalidRequests(std::vector<ApiRequestLog>& logs);
  • 呼出側責任 → 破壊を許容する
  • ストレージ効率重視

パターンB:抽出系API契約

std::vector<ApiRequestLog> extractSuccessRequests(const std::vector<ApiRequestLog>& logs);
  • 呼出側責任 → 状態を壊さない
  • 複数条件抽出用途に有効

copy_if / remove_if適用早見表

条件 推奨選択
元コレクション破壊許容 remove_if+erase
状態維持で新規抽出 copy_if
状態混濁懸念大 copy_if優先
サイズ縮小最優先 remove_if+erase

PlantUMLで設計責任整理

UML Diagram

観点チェックリスト

実務レビューFAQ

Q1. remove_ifだけで削除完了?
→ erase統合が必須。remove_if単体は前詰め処理のみ。

Q2. copy_ifはいつ積極利用?
→ 状態維持が要件に含まれる場合は優先設計。

Q3. 副作用は絶対NG?
→ ラムダ純粋関数維持が設計原則。状態集計は分離責任が安全。

Q4. API契約で破壊系/非破壊系分離は必須?
→ 設計安定性・汎用性を高める最重要設計文化。

Q5. remove_ifはerase前提文化を徹底?
→ 組織コーディング標準化を推奨。レビュー負荷が激減。

まとめ

copy_if / remove_ifはフィルタリング設計責任そのものである。
レビューアーは

  • 破壊責任整理
  • 状態責任整理
  • 副作用排除責任
  • API契約昇格設計

を読み解き、「操作ではなく責任でAPIを設計する」技術を養う必要がある。
レビューアーがフィルタリング責任整理レビューに強くなると設計品質は一段上がる。