この記事のポイント

  • C++における標準コンテナ選定の根拠をレビューアー視点で説明
  • vector・list・dequeを選ぶ際の実務的な基準を整理
  • データ規模・更新頻度・キャッシュ性能など多角的な観点を学べる

そもそもコンテナ選定とは

C++では標準ライブラリ(STL)が多様なコンテナを提供しています。vectorlistdequeなどはその代表格です。

  • vector: 連続領域。高速なランダムアクセスと末尾追加性能が特長
  • list: 双方向リンクリスト。要素の挿入削除に強いがランダムアクセスに弱い
  • deque: 両端キュー。vectorとlistの中間的特性を持つ

コンテナ選定は単なる好みや慣習で行うべきではありません。データ構造の選択は、計算量・CPUキャッシュヒット率・予測されるユースケースに直接影響を与えます。
レビューアーは、選定の根拠を設計意図として読み解くことが重要です。

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

レビューアー視点

  • 将来のデータ規模を想定した上で選定されているか
    数百件ならvector、数百万件ならlistという単純な話ではない。キャッシュフレンドリ性やアルゴリズム特性を踏まえているか。

  • 更新頻度・位置を考慮しているか
    中央挿入が多発する処理ならlist、末尾追記だけならvector、先頭操作が多ければdequeが候補になる。

  • アルゴリズムとの整合性を取っているか
    sort、search、merge等の利用を前提とした設計か。

  • パフォーマンスではなく「設計責務」で整理されているか
    配列操作を隠蔽したドメインクラスが提供されているか。

開発者視点

開発者はつい以下のように短絡的に選びがちです。

  • とりあえずvector
  • listを使えばinsertが速いらしい
  • dequeは知らないから使わない

こういった短絡選択を避けるため、開発者自身もコンテナ選定のロジックを持ち、コードにその意図が滲む設計が理想です。

良い実装例

以下はApiRequestLogのバッチ登録処理を想定した実装例です。ユースケースは「リクエストログを時系列順に格納し、バッチで後続処理に渡す」ものです。

良い実装例:ユースケース駆動のvector選定
#include <vector>
#include <string>
#include <cstdint>

// ログ1件を表すドメイン
struct ApiRequestLog {
    std::string requestId;
    std::string endpoint;
    std::string clientIp;
    int responseCode;
    int64_t requestedAt;
};

// ログバッファ管理クラス
class RequestLogBuffer {
public:
    void add(const ApiRequestLog& log) {
        buffer_.push_back(log);
    }

    const std::vector<ApiRequestLog>& getAll() const {
        return buffer_;
    }

    void clear() {
        buffer_.clear();
    }

private:
    std::vector<ApiRequestLog> buffer_;
};

このコードでは、

  • 常に末尾追加のみ → vectorpush_backが高速
  • 読み出し時は全件走査 → 連続メモリによるキャッシュ効率が良い
  • 頻繁な挿入削除や中央挿入が発生しない

というユースケースに対して、適切にvectorを選択しています。

レビュー観点

レビューでは以下の観点が重要です。

  • 連続メモリが欲しい理由はあるか?(例:キャッシュ効率)
  • 中央付近の挿入削除は発生するか?
  • データ規模はどの程度か?
  • 全件ソート・検索を前提としているか?
  • 先頭挿入が必要ならdequeを検討しているか?
  • コンテナの選定理由がドキュメント・コメントに残されているか?

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

以下は典型的に「listを何となく選んでしまった」ケースです。

誤ったlist選定例
#include <list>
#include <string>
#include <cstdint>

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

class RequestLogBuffer {
public:
    void add(const ApiRequestLog& log) {
        buffer_.push_back(log);
    }

    const std::list<ApiRequestLog>& getAll() const {
        return buffer_;
    }

    void clear() {
        buffer_.clear();
    }

private:
    std::list<ApiRequestLog> buffer_;
};
@Reviewer
データは時系列に追加されるだけの用途です。listではなくvectorの方がメモリ局所性が高く、実行時性能も安定します。vectorへの変更を検討してください。

問題点

  • 常に末尾追加のみならlistの利点は皆無
  • listはランダムアクセス不可
  • メモリ分散によるキャッシュミス多発
  • listのノード分割によるヒープ断片化

改善例

修正例:vector適用によるキャッシュ効率向上
#include <vector>
#include <string>
#include <cstdint>

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

class RequestLogBuffer {
public:
    void add(const ApiRequestLog& log) {
        buffer_.push_back(log);
    }

    const std::vector<ApiRequestLog>& getAll() const {
        return buffer_;
    }

    void clear() {
        buffer_.clear();
    }

private:
    std::vector<ApiRequestLog> buffer_;
};
なぜvectorか
  • 挿入は常に末尾固定
  • キャッシュ効率(局所性)が高い
  • データ走査性能が安定
  • listに比べメモリアロケーションが大幅に減少

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

次は「先頭挿入があるのにvectorを選んでしまった」例です。

誤ったvector選定例
#include <vector>
#include <string>
#include <cstdint>

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

class RequestLogBuffer {
public:
    void addToFront(const ApiRequestLog& log) {
        buffer_.insert(buffer_.begin(), log);
    }

    const std::vector<ApiRequestLog>& getAll() const {
        return buffer_;
    }

    void clear() {
        buffer_.clear();
    }

private:
    std::vector<ApiRequestLog> buffer_;
};
@Reviewer
先頭挿入が頻繁に発生するならdequeの方が適しています。vectorはinsert(begin())がO(N)でコスト高です。

問題点

  • 先頭挿入は毎回全要素シフトが必要
  • 頻度次第で性能劣化が著しい
  • 意図的にvectorを使うなら理由説明が必要

改善例

修正例:deque適用
#include <deque>
#include <string>
#include <cstdint>

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

class RequestLogBuffer {
public:
    void addToFront(const ApiRequestLog& log) {
        buffer_.push_front(log);
    }

    const std::deque<ApiRequestLog>& getAll() const {
        return buffer_;
    }

    void clear() {
        buffer_.clear();
    }

private:
    std::deque<ApiRequestLog> buffer_;
};
dequeの特性
  • 先頭・末尾の両方に高速な追加削除
  • 内部は分割連続領域の管理
  • 完全な連続メモリではないがvectorより柔軟性

PlantUMLによる用途整理

UML Diagram

観点チェックリスト

まとめ

コンテナ選定は、目先のコーディング効率ではなく、設計意図を反映する重要な設計判断です。
レビューアーは、データ流通量・操作頻度・API提供意図・パフォーマンス特性を複合的に読み解き、開発者の選択理由を確認する役割を持ちます。
今回のvector、list、dequeの整理は、その基礎となる設計思考の一例です。
今後のレビューではぜひ「なぜこのコンテナ?」を常に意識してみてください。