M.Hiroi's Home Page

お気楽C++プログラミング超入門

応用編 : ベクタクラスの作成 (テンプレート編)


Copyright (C) 2015-2023 Makoto Hiroi
All rights reserved.

はじめに

今回は簡単な例題として、以前作成したベクタクラス IntVec をテンプレートを使って書き直してみましょう。名前は Vector とします。なお、C++の標準ライブラリには <vector> や <array> が用意されているので、私たちが Vector クラスを作成する必要はありませんが、テンプレートのお勉強ということで、あえてプログラムを作ってみましょう。

●Vector クラスの定義

さっそくプログラムを作りましょう。Vector の定義は次のようになります。

リスト : クラス Vector の定義

// 宣言
template<class T> class Vector;
template<class T> ostream& operator<<(ostream&, const Vector<T>&);

// ベクタクラス
template<class T> class Vector {
  T* buff;
  size_t buff_size;
public:
  explicit Vector(size_t n) : buff(new T [n]), buff_size(n) { }
  ~Vector() { delete[] buff; }
  Vector(const Vector&);            // コピーコンストラクタ
  Vector& operator=(const Vector&); // 代入演算子
  Vector(Vector&&);                 // ムーブコンストラクタ
  Vector& operator=(Vector&&);      // ムーブ代入演算子
  T& operator[](int);               // 添字演算子
  // 出力演算子 << の後ろに <T> が必要
  friend std::ostream& operator<< <T> (std::ostream&, const Vector&);
  // その他のメンバ関数
  size_t size() const { return buff_size; }
  //
  // ・・・イテレータは省略・・・
  //
};

Vector で出力演算子を多重定義する場合、関数テンプレートで定義することになります。関数がテンプレートで、それをクラスで friend 宣言する場合、参考 URL: Sun Studio 12: C++ ユーザーズガイド 6.7.3 テンプレート関数のフレンド宣言 によると、あらかじめその関数がテンプレートであることを宣言する必要があるそうです。詳細は参考 URL をお読みください。

宣言の方法は簡単で、プロトタイプの前に template<class T, ...> を付けるだけです。たとえば、template<class T> Vector; とすれば、Vector はテンプレートであることが宣言されます。関数を宣言するときも同じです。 operator<< の前に template <class T> を付けます。引数のデータ型 Vector はテンプレートなので、後ろに <T> を付けます。それから、Vector 内の friend 宣言では、operator<< の後ろに <T> を付けてください。

operator << の定義は次のようになります。

リスト : 出力演算子の多重定義

template<class T>
ostream& operator<<(ostream& output, const Vector<T>& v)
{
  output << "[";
  int i = 0;
  for (; i < v.size - 1; i++)
    output << v.buff[i] << ",";
  output << v.buff[i] << "]";
  return output;
}

テンプレート仮引数 T を使ってデータ型を記述します。Vector の外側で定義するので、クラス Vector のデータ型は Vector<T> になります。<T> を付け忘れないように注意してください。

●メンバ関数の定義

次はメンバ関数を作ります。

リスト : メンバ関数

// コピーコンストラクタ
template<class T>
Vector<T>::Vector(const Vector<T>& v) : buff(new T [v.size]), size(v.size) 
{
  for (int i = 0; i < size; i++) buff[i] = v.buff[i];
}

// 代入演算子
template<class T>
Vector<T>& Vector<T>::operator=(const Vector<T>& v)
{
  if (this != &v) {
    if (size != v.size) {
      delete[] buff;
      size = v.size;
      buff = new T [size];
    }
    for (int i = 0; i < size; i++) buff[i] = v.buff[i];
  }
  return *this;
}

// ムーブコンストラクタ
template<class T>
Vector<T>::Vector(Vector&& v)
  : buff(v.buff), buff_size(v.buff_size)
{
  v.buff = nullptr;
  v.buff_size = 0;
}

// ムーブ代入演算子
template<class T>
Vector<T>& Vector<T>::operator=(Vector&& v)
{
  if (this != &v) {
    delete[] buff;
    buff = v.buff;
    buff_size = v.buff_size;
    v.buff = nullptr;
    v.buff_size = 0;
  }
  return *this;
}

// 配列添字演算子
template <class T>
T& Vector<T>::operator[](int i)
{
  if (i < 0 || i >= size) throw out_of_range("Vector: out of range");
  return buff[i];
}

関数定義の先頭に template<class T> を付けて、スコープ解決演算子でクラス名を指定します。このとき、クラス名は Vector<T> になります。あとはとくに難しいところはないでしょう。

●イテレータの定義

次はイテレータを作ります。自作のコンテナクラスにイテレータを定義する場合、STL の <iterator> に用意されているテンプレートクラス iterator を public で継承します。iterator には複数のテンプレート仮引数がありますが、最初の 2 つ (イテレータの種類、データ型) を必ず指定してください。あとの仮引数はデフォルト値が設定されているので、通常はそのままで大丈夫です。

イテレータの種類は次のデータで指定します。

Vector はランダムアクセスができるので、ランダムイテレータを実装します。イテレータは内部クラスで定義すると簡単です。次のリストを見てください。

リスト : イテレータ

  class Iterator : public iterator<random_access_iterator_tag, T> {
    Vector* vec;
    int idx;
  public:
    // コンストラクタ
    Iterator(Vector* v, int n) : vec(v), idx(n) { }
    // 間接参照 *, ->
    // ++, -- 演算子 (前置、後置)
    // +=, -=, +, - 演算子
    // 比較演算子 ==, !=, < <=, > >=
  };
  Iterator begin() { return Iterator(this, 0); }
  Iterator end() { return Iterator(this, buff_size); }
表 : イテレータで多重定義する演算子
演算子機能
T& operator*()間接参照演算子
T* operator->()要素がクラス (構造体) の場合、メンバを選択する
Iterator& operator++()前置の ++ 演算子
Iterator operator++(int n)後置の ++ 演算子はイテレータのコピーを返す (引数はダミー)
Iterator& operator--()前置の -- 演算子
Iterator operator--(int n)後置の -- 演算子はイテレータのコピーを返す (引数はダミー)
Iterator& operator+=(size_t n)イテレータを n 個進める
Iterator& operator-=(size_t n)イテレータを n 個戻す
Iterator operator+(size_t n)n 個進めた新しいイテレータを返す
Iterator operator-(size_t n)n 個戻した新しいイテレータを返す
bool operator==(const Iterator& iter) constiter と等しいとき真を返す
bool operator!=(const Iterator& iter) constiter と等しくないとき真を返す
bool operator<(const Iterator& iter) constiter よりも小さいとき真を返す
bool operator<=(const Iterator& iter) constiter 以下のときに真を返す
bool operator>(const Iterator& iter) constiter よりも大きいとき真を返す
bool operator>=(const Iterator& iter) constiter 以上のとき真を返す

多重定義する演算子の仕様を上表に示します。プログラムは簡単なので説明は割愛します。詳細はプログラムリストをお読みください。

●簡単なテスト

それは実際に実行してみましょう。次に示す簡単なテストを行ってみました。

リスト : 簡単なテスト

int main()
{
  Vector<int> a(8);
  for (int i = 0; i < a.size(); i++) a[i] = i;
  Vector<int> b = a;
  for (int i = 0; i < b.size(); i++) b[i] *= 2;
  cout << a << endl;
  cout << b << endl;
  {
    Vector<int> c = a;
    a = b;
    b = c;
  }
  cout << a << endl;
  cout << b << endl;
  {
    Vector<int> c = move(a);
    a = move(b);
    b = move(c);
  }
  cout << a << endl;
  cout << b << endl;
  for (auto iter = a.begin(); iter < a.end(); iter += 2)
    cout << *iter << " ";
  cout << endl;
  auto iter = b.begin();
  while (iter != b.end())
    cout << *iter++ << " ";
  cout << endl;
  for (auto x : a) cout << x << " ";
  cout << endl;
  for_each(a.begin(), a.end(), [](int x){ cout << x << " "; });
  cout << endl;
  Vector<pair<string,int>> c(4);
  c[0].first = "foo";
  c[0].second = 1;
  c[1].first = "bar";
  c[1].second = 2;
  c[2].first = "baz";
  c[2].second = 3;
  c[3].first = "oops";
  c[3].second = 4;
  for (auto iter = c.begin(); iter != c.end(); iter++)
    cout << iter->first << "," << iter->second << endl;
}
$ clang++ vector.cpp
$ ./a.out
[0,1,2,3,4,5,6,7]
[0,2,4,6,8,10,12,14]
[0,2,4,6,8,10,12,14]
[0,1,2,3,4,5,6,7]
[0,1,2,3,4,5,6,7]
[0,2,4,6,8,10,12,14]
0 2 4 6
0 2 4 6 8 10 12 14
0 1 2 3 4 5 6 7
0 1 2 3 4 5 6 7
foo,1
bar,2
baz,3
oops,4

コピーコンストラクタ、代入演算子、ムーブコンストラクタ、ムーブ代入演算子は正常に動作しています。添字演算子も大丈夫ですね。STL の仕様に合わせてイテレータを実装すると、範囲 for 文や、for_each() など algorithm の関数も利用することができます。

Vector に pair<string, int> を格納することもできます。このとき、pair のデフォルトコンストラクタによりベクタが初期化されます。ベクタを廃棄するときは、pair のデストラクタが実行されます。-> で pair のメンバ変数 first, second を選択することができます。興味のある方はいろいろ試してみてください。


●プログラムリスト

//
// vector.cpp : ベクタクラス (テンプレート版)
//
//              Copyright (C) 2015-2023 Makoto Hiroi
//
#include <iostream>
#include <stdexcept>
#include <iterator>
#include <algorithm>
using namespace std;

// 宣言
template<class T> class Vector;
template<class T> ostream& operator<<(ostream&, const Vector<T>&);

// ベクタクラス
template<class T> class Vector {
  T* buff;
  size_t buff_size;
public:
  explicit Vector(size_t n) : buff(new T [n]), buff_size(n) { }
  ~Vector() { delete[] buff; }
  Vector(const Vector&);            // コピーコンストラクタ
  Vector& operator=(const Vector&); // 代入演算子
  Vector(Vector&&);                 // ムーブコンストラクタ
  Vector& operator=(Vector&&);      // ムーブ代入演算子
  T& operator[](int);               // 添字演算子
  // 出力演算子 << の後ろに <T> が必要
  friend std::ostream& operator<< <T> (std::ostream&, const Vector&);
  // その他のメンバ関数
  size_t size() const { return buff_size; }

  // イテレータ
  class Iterator : public iterator<random_access_iterator_tag, T> {
    Vector* vec;
    int idx;
  public:
    // コンストラクタ
    Iterator(Vector* v, int n) : vec(v), idx(n) { }
    // 間接参照
    T& operator*()  { return vec->buff[idx]; }
    T* operator->() { return &vec->buff[idx]; }
    // 前置の ++, -- 演算子
    Iterator& operator++() {
      idx++;
      return *this;
    }
    Iterator& operator--() {
      idx--;
      return *this;
    }
    // 後置の ++, -- 演算子, 引数はダミー
    Iterator operator++(int n) {
      Iterator iter(*this);
      idx++;
      return iter;
    }
    Iterator operator--(int n) {
      Iterator iter(*this);
      idx--;
      return iter;
    }
    // +=, -=
    Iterator& operator+=(size_t n) {
      idx += n;
      return *this;
    }
    Iterator& operator-=(size_t n) {
      idx -= n;
      return *this;
    }
    // +, -
    Iterator operator+(size_t n) {
      return Iterator(vec, idx + n);
    }
    Iterator operator-(size_t n) {
      return Iterator(vec, idx - n);
    }
    // 比較演算子
    bool operator==(const Iterator& iter) const {
      return vec == iter.vec && idx == iter.idx;
    }
    bool operator!=(const Iterator& iter) const {
      return vec != iter.vec || idx != iter.idx;
    }
    bool operator<(const Iterator& iter) const {
      return vec == iter.vec && idx < iter.idx;
    }
    bool operator<=(const Iterator& iter) const {
      return vec == iter.vec && idx <= iter.idx;
    }
    bool operator>(const Iterator& iter) const {
      return vec == iter.vec && idx > iter.idx;
    }
    bool operator>=(const Iterator& iter) const {
      return vec == iter.vec && idx >= iter.idx;
    }
  };

  Iterator begin() { return Iterator(this, 0); }
  Iterator end() { return Iterator(this, buff_size); }
};  

// コピーコンストラクタ
template<class T>
Vector<T>::Vector(const Vector& v)
  : buff(new T [v.buff_size]), buff_size(v.buff_size)
{
  for (int i = 0; i < buff_size; i++) buff[i] = v.buff[i];
}

// 代入演算子
template<class T>
Vector<T>& Vector<T>::operator=(const Vector<T>& v)
{
  if (this != &v) {
    delete[] buff;
    buff_size = v.buff_size;
    buff = new T [buff_size];
    for (int i = 0; i < buff_size; i++) buff[i] = v.buff[i];
  }
  return *this;
}

// ムーブコンストラクタ
template<class T>
Vector<T>::Vector(Vector&& v)
  : buff(v.buff), buff_size(v.buff_size)
{
  v.buff = nullptr;
  v.buff_size = 0;
}

// ムーブ代入演算子
template<class T>
Vector<T>& Vector<T>::operator=(Vector&& v)
{
  if (this != &v) {
    delete[] buff;
    buff = v.buff;
    buff_size = v.buff_size;
    v.buff = nullptr;
    v.buff_size = 0;
  }
  return *this;
}

// 添字
template <class T>
T& Vector<T>::operator[](int i)
{
  if (i < 0 || i >= buff_size)
    throw std::out_of_range("Vector: out of range");
  return buff[i];
}

// 出力
template<class T>
std::ostream& operator<<(std::ostream& output, const Vector<T>& v)
{
  output << "[";
  int i = 0;
  for (; i < v.buff_size - 1; i++)
    output << v.buff[i] << ",";
  output << v.buff[i] << "]";
  return output;
}

// 簡単なテスト
int main()
{
  Vector<int> a(8);
  for (int i = 0; i < a.size(); i++) a[i] = i;
  Vector<int> b = a;
  for (int i = 0; i < b.size(); i++) b[i] *= 2;
  cout << a << endl;
  cout << b << endl;
  {
    Vector<int> c = a;
    a = b;
    b = c;
  }
  cout << a << endl;
  cout << b << endl;
  {
    Vector<int> c = move(a);
    a = move(b);
    b = move(c);
  }
  cout << a << endl;
  cout << b << endl;
  for (auto iter = a.begin(); iter < a.end(); iter += 2)
    cout << *iter << " ";
  cout << endl;
  auto iter = b.begin();
  while (iter != b.end())
    cout << *iter++ << " ";
  cout << endl;
  for (auto x : a) cout << x << " ";
  cout << endl;
  for_each(a.begin(), a.end(), [](int x){ cout << x << " "; });
  cout << endl;
  Vector<pair<string,int>> c(4);
  c[0].first = "foo";
  c[0].second = 1;
  c[1].first = "bar";
  c[1].second = 2;
  c[2].first = "baz";
  c[2].second = 3;
  c[3].first = "oops";
  c[3].second = 4;
  for (auto iter = c.begin(); iter != c.end(); iter++)
    cout << iter->first << "," << iter->second << endl;
}

初版 2015 年 10 月 24 日
改訂 2023 年 4 月 15 日