プログラミング実習 III (クラス 4)

[B10] C++ (2) 「標準ライブラリ STL」

今回の実習では、 C++ の強力な武器である「STL (Standard Template Library)」を学ぶ。 これまでの C 言語によるプログラミングでは、 配列のメモリ管理やリスト構造のポインタ操作など、 データ構造の「実装」そのものに多くの労力を割いてきた。

しかし、 C++ の STL を利用することで、 これらの低レイヤの管理から解放され、 より本質的な「アルゴリズム」や「アプリケーションのロジック」に集中できるようになる。

今回の課題に着手する前に、 以下の要点を整理し、 それぞれのツールの特性を理解しておこう。

今回の課題のねらい

今回の課題の到達目標

重要な C++ の概念

今回の課題の範囲で特に重要な概念を説明しておこう。

テンプレート (Template)

テンプレート利用の例:

#include <iostream>
#include <vector>

template <typename T>
T add(T a, T b) {
  return a + b;
}

int main() {
  std::cout << add<int>(3, 5) << std::endl;  // 8
  std::cout << add<double>(1.2, 3.4) << std::endl;  // 4.6

  std::vector<int> v = {1, 2, 3};
  std::cout << v.size() << std::endl;  // 3
}

概念: データ型に依存しない汎用的なクラスや関数を作る仕組み。 vector<int> や vector<string> のように山括弧内に型を指定することで、 特定の型専用のクラスをコンパイラが生成する。

重要性: 型の安全性 (Type Safety) を保ちつつ、 コードの再利用性を高める。 C 言語の void* を用いた汎用化とは異なり、 コンパイル時に型チェックが行われるため安全である。

C 言語では? : C 言語では、 (1) 型ごとに関数を別々に実装する (コード重複)、 (2) void * とサイズ引数を用いた汎用関数 (例: qsort)、 (3) C のプリプロセッサを使ったマクロ展開、 などの方法で書く必要があった (C++ 言語では、C 言語から大幅に使い勝手が向上している)。

コンテナ (Container)

コンテナ利用の例:

#include <iostream>
#include <string>
#include <vector>
#include <list>
#include <map>

int main() {
  // vector: 可変長配列(末尾追加が容易)
  std::vector<int> v = {1, 2, 3};
  v.push_back(4);
  std::cout << "vector size = " << v.size() << std::endl;

  // list: 連結リスト(途中の挿入・削除が容易)
  std::list<std::string> lst = {"Alice", "Bob"};
  lst.push_front("Zoe");
  lst.remove("Bob");  // 値に一致する要素を削除
  std::cout << "list size = " << lst.size() << std::endl;

  // map: 連想配列(キー→値)
  std::map<std::string, int> score;
  score["Alice"] = 90;  // [] は存在しないキーなら要素を生成する点に注意
  score["Bob"] = 75;

  // 走査(イテレータ)
  for (std::map<std::string, int>::iterator it = score.begin(); it != score.end(); ++it) {
    std::cout << it->first << ": " << it->second << std::endl;
  }

  return 0;
}

概念: オブジェクトを格納するデータ構造の総称。 vector (可変長配列)、 list (連結リスト)、 map (連想配列)などが含まれる。

重要性: メモリ管理が自動化されており、 領域の拡張や縮小、 再配置 (reallocation) が隠蔽されている。 ユーザはサイズを意識せずにデータを追加できる。 コンテナを用いることで、 データ構造の実装(メモリ確保・解放、 境界管理、 ポインタ操作など)を自前で書く必要がなくなり、 要素の追加・削除・検索といった操作を安全かつ簡潔に記述できるようになる。 また、 コンテナが提供する共通インタフェース( begin(), end() など)により、 sortfind といったアルゴリズムを「データ構造の種類に依存せず」再利用できる(実装と利用の分離)。

C 言語では? : 「コンテナ」という抽象化は基本的に用意されていないため、 用途に応じたデータ構造を自分で実装する必要があった。 固定長配列や可変長配列を利用するか、 連結リストや連想配列を自分で実装するか外部のライブラリを使う必要があった。

イテレータ (Iterator / 反復子)

イテレータ利用の例:

#include <iostream>
#include <vector>
#include <list>

int main() {
  // vector の走査(ランダムアクセス可能)
  std::vector<int> v = {10, 20, 30};

  // 典型例: begin() から end() まで ++ で進め、* で要素参照
  for (std::vector<int>::iterator it = v.begin(); it != v.end(); ++it) {
    std::cout << *it << std::endl;
  }

  // list の走査(ランダムアクセス不可だが、同じ書き方で走査できる)
  std::list<int> lst = {1, 2, 3};
  for (std::list<int>::iterator it = lst.begin(); it != lst.end(); ++it) {
    std::cout << *it << std::endl;
  }

  // const イテレータ(読み取り専用)
  const std::vector<int> cv = {100, 200};
  for (std::vector<int>::const_iterator it = cv.begin(); it != cv.end(); ++it) {
    std::cout << *it << std::endl;
  }

  return 0;
}

概念: コンテナ内の要素を指し示し、 移動するためのオブジェクト。 「ポインタを抽象化したもの」と捉えるとよい。

重要性: 内部構造が異なるコンテナに対しても、 begin(), end(), ++, * といった共通操作を提供する。 これにより、 アルゴリズムをコンテナの実装から分離できる。

C 言語では? : C 言語では汎用のコンテナが存在しないため、 C 言語はイテレータに存在するものを持っていない。 配列に対してはポインタ( int * など)をインクリメントして走査するのが最も近い。 ただし、 これは「連続メモリである」という前提に強く依存する。 このため、 連結リスト等の非連続なデータ構造では、 structnext ポインタを持たせて辿るなど、 データ構造ごとに走査方法(アクセス手段)を個別に実装する必要があった。

関数オブジェクト (Function Object)

関数オブジェクト利用の例:

#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

// 関数オブジェクト(比較器): 文字列長で昇順ソート
struct ByLength {
  bool operator()(const std::string& a, const std::string& b) const {
    return a.size() < b.size();
  }
};

// 関数オブジェクト(状態を持つ述語): 閾値以上かどうか
class AtLeast {
 public:
  explicit AtLeast(int threshold) : threshold_(threshold) {}
  bool operator()(int x) const { return x >= threshold_; }

 private:
  int threshold_;
};

int main() {
  // sort に比較規則を渡す(関数オブジェクト)
  std::vector<std::string> words = {"network", "AI", "STL", "template"};
  std::sort(words.begin(), words.end(), ByLength());
  for (std::vector<std::string>::const_iterator it = words.begin(); it != words.end(); ++it) {
    std::cout << *it << std::endl;
  }

  // find_if に「状態を持つ」条件を渡す(threshold=10 を埋め込む)
  std::vector<int> v = {3, 7, 10, 12};
  std::vector<int>::iterator it = std::find_if(v.begin(), v.end(), AtLeast(10));
  if (it != v.end()) {
    std::cout << "first >= 10: " << *it << std::endl;
  }

  return 0;
}

概念: 関数のように振る舞うオブジェクト( operator() をオーバーロードしたクラス)。 例えば、 sort などに比較規則として渡すことで、 関数ポインタより柔軟な処理が可能となる。

重要性: 関数オブジェクトは「状態 (state) を持てる関数」である点が重要である。 関数ポインタと異なり、 メンバ変数としてパラメータ(閾値、 重み、 辞書、 正規化規則など)を保持できるため、 同じアルゴリズム呼び出しでも「設定を埋め込んだ振る舞い」を安全に渡せる。

C 言語では? : C 言語において、 アルゴリズムに「処理(振る舞い)」を渡したい場合は、 関数ポインタ( function pointer )を渡すという方法があった。

STL で注意すべきポイント

STL は非常に便利であるが、 その裏側で何が起きているかを理解していないと、 非効率なコードやバグの原因となる。


Hiroyuki Ohsaki (ohsaki[atmark]lsnl.jp)