この記事のポイント

  • unique/eraseによる重複排除戦略をレビューアー視点で体系整理
  • 前提条件(ソート保証)の契約整理を設計レビューで読み取る
  • API契約・性能責任・副作用責任の設計レビューを実戦的に習得

そもそもunique/erase戦略とは

C++標準ライブラリにおける重複排除手法の代表が

auto last = std::unique(first, last);
container.erase(last, container.end());

という remove-eraseイディオムの特殊形態である。

メソッド 役割
unique 連続重複を前詰め(ソート前提)
erase 前詰め後の不要領域削除
  • uniqueは隣接重複のみ排除
  • ソート保証が設計責任の中心

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

レビューアー視点

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

  • ソート保証責任
    → 前処理設計有無確認

  • 比較責任の設計整理
    → ==利用/カスタム述語設計確認

  • 副作用排除責任
    → erase位置計算責任を維持できているか

  • API契約明文化
    → 排除条件が設計契約として文書化されているか

  • 性能責任
    → 件数・重複密度次第で構築系集合へ昇格提案検討

開発者視点

  • ソート前提忘却
  • unique単独利用誤認
  • remove_ifと混同
  • API契約責任放置
  • unordered_set移行タイミング不明

レビューアーはこれら「責任抜け落ち実装」を静かに発見する役割を担う。

良い実装例

ユースケース:APIレスポンスIDの重複排除

良い実装例:ソート保証付unique/erase設計
#include <vector>
#include <string>
#include <algorithm>

std::vector<std::string> deduplicate(std::vector<std::string> ids) {
    std::sort(ids.begin(), ids.end());
    auto last = std::unique(ids.begin(), ids.end());
    ids.erase(last, ids.end());
    return ids;
}
  • 事前ソート保証責任が内部保持
  • 副作用ゼロ
  • 契約整理が外部APIに波及しない

レビュー観点

  • ソート保証がunique適用前に整理されているか
  • erase位置計算責任が正しく適用されているか
  • 戻り契約に重複排除済が明文化されているか
  • 件数規模が集合型導入閾値に達していないか

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

以下はソート保証を省略しuniqueを安易適用している例。

ソート保証放置例
std::vector<std::string> deduplicate(std::vector<std::string> ids) {
    auto last = std::unique(ids.begin(), ids.end());
    ids.erase(last, ids.end());
    return ids;
}
@Reviewer
uniqueは隣接重複前提です。ソート保証をAPI設計内に持たせてください。

問題点

  • 非連続重複が残存
  • 設計責任崩壊

改善例

修正例:事前ソート設計
std::sort(ids.begin(), ids.end());
auto last = std::unique(ids.begin(), ids.end());
ids.erase(last, ids.end());

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

次はuniqueだけで重複排除が完結する誤認している例。

uniqueのみ誤認例
std::unique(ids.begin(), ids.end());
@Reviewer
uniqueは前詰め処理のみで削除は行いません。eraseまで設計責任を保ってください。

問題点

  • サイズ不一致
  • 不要領域放置

改善例

修正例:erase統合
ids.erase(std::unique(ids.begin(), ids.end()), ids.end());

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

次は件数規模が巨大化してもvector+uniqueに固執している例。

巨大件数誤用例
std::vector<std::string> ids = load();
std::sort(ids.begin(), ids.end());
ids.erase(std::unique(ids.begin(), ids.end()), ids.end());
@Reviewer
件数規模次第でunordered_set等の集合構造適用検討が可能です。

問題点

  • O(N logN)ソート負荷が高騰
  • ハッシュ構造利用でO(N)可能性放棄

改善例

修正例:unordered_set活用
std::unordered_set<std::string> idsSet(source.begin(), source.end());
std::vector<std::string> ids(idsSet.begin(), idsSet.end());
  • 件数規模で構造切替をレビュー側が提案

unique/erase戦略選択早見表

条件 推奨戦略
件数小規模 sort+unique+erase
件数中規模 同上(性能大差なし)
件数大規模 unordered_set活用

API契約整理パターン

パターンA:内部責任保持

std::vector<std::string> deduplicate(std::vector<std::string> ids);
  • API内部でソート保証実施
  • 呼出契約は単純

パターンB:呼出責任移譲

void deduplicateSorted(std::vector<std::string>& ids);
  • 呼出側がソート責任保持
  • 契約がレビュー時に明文化

unique/erase vs unordered_set 設計責任整理

項目 unique/erase unordered_set
事前ソート 必須 不要
安定性 並び変わる ハッシュ順序
容易性 簡便 構築責任増加
件数閾値 小〜中 中〜大
  • 件数規模+API契約+ライフサイクルで設計判断

PlantUMLで設計責任整理

UML Diagram

観点チェックリスト

実務レビューFAQ

Q1. uniqueだけで重複排除は完了?
→ 前詰め処理のみ。erase統合が必須。

Q2. unordered_setは常に上位互換?
→ 件数規模・構築コスト・順序保証要件次第。

Q3. APIでソート責任は委譲すべき?
→ 頻繁用途なら委譲、一般APIなら内部責任保持が安全。

Q4. unordered_setはreserve設計必要?
→ 高負荷ではreserve設計が性能安定化に有効。

Q5. 重複定義を比較関数で制御可能?
→ uniqueでもunordered_setでもカスタム比較/ハッシュで柔軟設計可能。

まとめ

unique/erase戦略レビューはソート保証責任とライフサイクル設計の縮図である。
レビューアーは

  • ソート責任
  • erase統合責任
  • 件数規模判定
  • API契約反映

を読み解き、「重複排除設計はAPI契約責任整理の訓練場」と捉えてレビューを進める必要がある。
レビューアーがunique/erase責任を正しく整理できるとAPI設計品質が段違いに向上する。