M.Hiroi's Home Page

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

中級編 : ベクタクラスの作成


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

はじめに

今回は簡単な例題として、1 次元配列を表すクラス (vector : ベクタ) を作ってみましょう。なお、C++の標準ライブラリには <vector> という可変長配列や、固定サイズの配列をクラスにした <array> が用意されています。私たちが vector を作る必要はありませんが、C++のお勉強ということで、あえてプログラムを作ってみましょう。

●ベクタクラスの定義

今回はプログラムを簡単にするため、格納するデータ型は int に限定します。クラス名は IntVec としました。C++の場合、「テンプレート (template)」[*1] という機能を使うと、いろいろなデータ型に対応することができます。これはテンプレートを説明したあとで試してみましょう。

最初は、ベクタの基本的な機能のみを実装します。要素のアクセスは、配列と同様に配列添字演算子 [] を使うことにします。これは演算子を多重定義すれば、簡単に実装することができます。次のリストを見てください。

リスト : クラス IntVec の定義

class IntVec {
  int *buff;
  size_t size;
public:
  explicit IntVec(size_t n) : buff(new int [n]), size(n) { }
  ~IntVec() { delete[] buff; }
  IntVec(const IntVec&);            // コピーコンストラクタ
  IntVec& operator=(const IntVec&); // 代入演算子
  int& operator[](int);             // 配列添字演算子
  // 出力演算子
  friend ostream& operator<<(ostream&, const IntVec&);
};

メンバ変数 buff にベクタの本体を、size にベクタの大きさを格納します。size_t は unsigned int の別名です。コンストラクタは引数としてベクタの大きさ n を受け取り、new で大きさ n のベクタを生成します。キーワード explicit を付けて、型変換が行われないようにしています。デストラクタは delete[] で取得したメモリ領域を解放するだけです。

メンバ変数がポインタなので、コピーコンストラクタと代入演算子の多重定義が必要になります。配列添字演算子の多重定義は簡単です。参照 (int&) を返すことで、たとえば a[0] = 1; のような代入も可能になります。最後に出力演算子を多重定義します。

-- note --------
[*1] C++の場合、標準ライブラリの多くはテンプレートで作成されています。これを「標準テンプレートライブラリ (Standard Template Library : STL)」といいます。<vector> や <array> も STL の一部です。

●メンバ関数の定義

次はメンバ関数を定義します。最初はコピーコンストラクタと代入演算子です。

リスト : メンバ関数の定義

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

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

コピーコンストラクタの場合、引数 v と同じ大きさのベクタを確保します。初期化リストで buff のメモリ領域を取得して、メンバ変数にセットします。あとは、引数 v の buff の内容を自分自身 (this) の buff にコピーするだけです。

代入演算子の場合、最初に自分自身の代入かチェックします。そうでなければ、引数 v の buff の内容を自分自身 (this) の buff にコピーします。ベクタの大きさが異なる場合、delete[] で buff を解放して、大きさ v.size のバッファを取得します。あとは for 文で、buff の要素をコピーするだけです。

次は、添字演算子と出力演算子を多重定義します。

リスト : メンバ関数の定義 (2)

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

// 出力演算子
ostream& operator<<(ostream& output, const IntVec& v)
{
  output << "[";
  int i = 0;
  for (; i < v.size - 1; i++)
    output << v.buff[i] << ",";
  output << v.buff[i] << "]";
  return output;
}

添字演算は簡単です。引数 i が buff の範囲内かチェックします。そうでなければ例外 out_of_range を送出します。あとは buff[i] を返すだけです。出力演算子は角カッコで囲んで、要素をカンマ ( , ) で区切って表示することにします。

●簡単な実行例

それでは簡単なテストを実行してみましょう。

リスト : 簡単なテスト

void test1()
{
  IntVec a(10);
  cout << a[0] << endl;
  cout << a[9] << endl;
  for (int i = 0; i < 10; i++) a[i] = i;
  cout << a << endl;
  IntVec b = a;
  cout << b << endl;
  b[0] = 10;
  b[9] = 20;
  cout << b << endl;
  cout << a << endl;
  IntVec c(5);
  c = a;
  c[0] = 100;
  cout << c << endl;
  cout << a << endl;
  IntVec d(15);
  d = a;
  d[0] = 200;
  cout << d << endl;
  cout << a << endl;
}
$ clang++ intvec.cpp
$ ./a.out
0
0
[0,1,2,3,4,5,6,7,8,9]
[0,1,2,3,4,5,6,7,8,9]
[10,1,2,3,4,5,6,7,8,20]
[0,1,2,3,4,5,6,7,8,9]
[100,1,2,3,4,5,6,7,8,9]
[0,1,2,3,4,5,6,7,8,9]
[200,1,2,3,4,5,6,7,8,9]
[0,1,2,3,4,5,6,7,8,9]

添字演算子だけではなく、コピーコンストラクタと代入演算子も正常に動作していますね。

●イテレータ

STL には <vector> や <array> 以外にも、双方向リスト (doubly-linked list) を実装した <list> や単方向リスト (singled-linked list) を実装した <forward_list> というコンテナクラスが用意されています。これらのコンテナは、配列と違ってデータを格納する「セル (Cell)」を連結したデータ構造になっています。このようなデータ構造を「連結リスト (linked list)」と呼びます。

連結リストはランダムなアクセスが大変苦手で、シーケンシャルにアクセスすることが普通です。このため、これらのクラスでは配列添字演算子が定義されていません。STL ではデータ構造の違いを吸収し、要素にアクセスする共通な仕組みとして「イテレータ (iterator)」が用意されています。イテレータは「反復子」と訳されることがありますが、最近は訳さずにそのまま使われることが多いようです。

イテレータはコンテナの要素を指し示すオブジェクトです。たとえば、コンテナのメンバ関数 begin は先頭要素を指し示すイテレータを生成し、end は末尾の次の要素 (終端) を指し示すイテレータを生成します。イテレータに ++ 演算子を適用すると、イテレータは次の要素に移動します。-- 演算子を適用すると、一つ前の要素に移動します。要素は間接演算子 * で読み書きすることができます。イテレータを比較する演算子も用意されていて、イテレータを使えばコンテナの種類にかかわらずコンテナの全要素にアクセスすることができます。

STL のイテレータはテンプレートを使っています。今回は簡単な例題なので、テンプレートは使わずに、独自な方法で IntVec にイテレータの基本的な機能を追加してみましょう。C++の標準的なイテレータは、テンプレートを取り上げるときに説明することにします。

●内部クラス

イテレータはコンテナクラスと密接に関連したクラスになります。イテレータを単独で使用することはありません。このような場合、コンテナクラスの内部でイテレータクラスを定義できると便利です。C++はクラス定義の中でクラスを定義することが可能です。これを「内部クラス (inner class)」とか「入れ子クラス」といいます。

C++の内部クラスは簡単で、単にクラスの中でクラスを定義するだけです。Java とは違って、外側のクラスのインスタンスが存在しなくても、内部クラスのインスタンスを生成することができます。また、内部クラスのメンバ関数は外側のクラスの private なメンバにアクセスすることができます。

簡単な例を示しましょう。次のリストを見てください。

リスト : 内部クラス (sample145.cpp)

#include <iostream>
using namespace std;

class Foo {
  int x;
public:
  Foo() : x(0) {}
  int get_x() const { return x; }
  void put_x(int n) { x = n; }

  class Bar {
    double y;
  public:
    Bar() : y(0) {}
    double get_y() const { return y; }
    void put_y(double n) { y = n; }
  };
};

int main()
{
  Foo a;
  cout << a.get_x() << endl;
  a.put_x(1);
  cout << a.get_x() << endl;
  Foo::Bar b;
  cout << b.get_y() << endl;
  b.put_y(1.2345);
  cout << b.get_y() << endl;
}
$ clang++ sample145.cpp
$ ./a.out
0
1
0
1.2345

クラス Bar はクラス Foo の中で定義されているので内部クラスになります。Foo の中で Bar を使用する場合は、今までとまったく同じです。Foo の外側で Bar を使う場合は演算子 :: を使って、外側のクラスを指定します。たとえば、Foo::Bar b; とすれば、変数 b に内部クラス Bar のインスタンスがセットされます。

●クラス Iterator の定義

それではイテレータを表すクラス Iterator を作りましょう。次のリストを見てください。

リスト : Iterator の定義

class IntVec {
  //
  // ・・・省略・・・
  //

  // イテレータ
  class Iterator {
    IntVec *vec;
    int idx;
  public:
    Iterator(IntVec* v, int n) : vec(v), idx(n) { }
    // 前置きの ++, -- 演算子
    Iterator& operator++() {
      if (idx < vec->size) idx++;
      return *this;
    }
    Iterator& operator--() {
      if (idx > 0) idx--;
      return *this;
    }
    // 間接演算子
    int& operator*() { return vec->buff[idx]; }
    // 比較演算子
    bool operator==(const Iterator& iter) {
      return vec == iter.vec && idx == iter.idx;
    }
    bool operator!=(const Iterator& iter) {
      return vec != iter.vec || idx != iter.idx;
    }
    bool operator<const Iterator& iter) {
      return vec == iter.vec && idx < iter.idx;
    }
    bool operator<=(const Iterator& iter) {
      return vec == iter.vec && idx <= iter.idx;
    }
    bool operator>(const Iterator& iter) {
      return vec == iter.vec && idx > iter.idx;
    }
    bool operator>=(const Iterator& iter) {
      return vec == iter.vec && idx >= iter.idx;
    }
  };

  // イテレータの生成
  Iterator begin() { return Iterator(this, 0); }
  Iterator end() { return Iterator(this, size); }
};

メンバ変数 vec はベクタクラスのインスタンスを指し示すポインタで、idx がベクタの要素を指し示す添字になります。vec はポインタ変数ですが、Iterator のコンストラクタでメモリを取得していないので、デストラクタで delete してはいけません。また、コピーコンストラクタと代入演算子もデフォルトのもので大丈夫です。

コンストラクタの第 1 引数 v は IntVec のインスタンスをポインタで受け取ります。第 2 引数 n が idx の初期値になります。実際には、IntVec::Iterator(v, n) を直接呼び出すのではなく、IntVec にイテレータを生成するメンバ関数 begin と end を用意して、そこでイテレータを生成します。

あとはイテレータの操作で必要となる演算子を多重定義します。++, -- 演算子は前置と後置がありますが、今回は前置だけを定義します。この場合、返り値の型は Iterator& になります。あとは、idx の値を +1 (または -1) するだけです。間接演算子も簡単で、返り値の型を参照型 (int&) に指定します。これで配列添字演算子と同様に、ベクタの要素を読み書きすることができます。

比較演算子も簡単です。ベクタの本体は連続したメモリ領域に配置されるので、イテレータは添字の大きさで大小関係を判定することができます。自分自身 (this) の vec と引数 iter の vec が等しくて、idx と vec.idx の大小関係が演算子の条件を満たしているならば true を返します。

●簡単な実行例 (2)

イテレータの基本はこれだけです。それでは実際に試してみましょう。

リスト : 簡単なテスト

void test2()
{
  IntVec a(10);
  int i = 1; 
  for (auto iter = a.begin(); iter < a.end(); ++iter)  
    *iter = i++;
  for (auto iter = a.begin(); iter < a.end(); ++iter)
    cout << *iter << endl;
}
$ clang++ intvec.cpp
$ ./a.out

・・・略・・・

1
2
3
4
5
6
7
8
9
10

for 文の変数 iter のデータ型は IntVec::Iterator になりますが、ちょっと長いので書くのが面倒です。最近の規格 (C++11) では、データ型の宣言で auto を指定すると、コンパイラがデータ型を推論してくれます。このように、イテレータを使ってコンテナの要素にアクセスすることができます。

●高階関数によるアクセス

ベクタの要素は高階関数を使ってアクセスすることも可能です。たとえば、ベクタの要素に引数の関数を適用する高階関数 for_each は次のように定義することができます。

リスト : 高階関数 for_each

class IntVec {
  //
  // ・・・省略
  //

  // 高階関数
  void for_each(void (*func)(int)) {
    for (int i = 0; i < size; i++) func(buff[i]);
  }
};

引数 func に関数ポインタを受け取り、for ループで順番に要素を取り出して func を呼び出すだけです。このように、for_each の内部でベクタの要素を取り出し、それを関数に渡して処理する方法を「内部イテレータ」と呼びます。Iterator のように、要素を指し示すオブジェクトを作り、それを操作して処理する方法を「外部イテレータ」と呼びます。

今回は for_each を IntVec のメンバ関数としましたが、次のように関数として定義することもできます。

リスト : 高階関数 for_each (2)

void for_each(IntVec& v, void (*func)(int))
{
  for (auto iter = v.begin(); iter < v.end(); ++iter)
    func(*iter);
}

ここでテンプレートを使うと、汎用的な for_each を作ることができます。実際、STL の <algorithm> にはテンプレートで作成された for_each が用意されています。このほかにも便利な高階関数が多数用意されています。

●関数オブジェクト

C++は関数呼び出し演算子 () を多重定義することができます。演算子 () を多重定義したクラスから生成されたインスタンスを「関数オブジェクト」といい、インスタンスにカッコを付けることで、多重定義した処理を関数のように呼び出すことができます。もちろん、引数を渡すことも可能です。

簡単な例を示しましょう。

リスト : 関数オブジェクト (sample146.cpp)

#include <iostream>
using namespace std;

class Foo {
  int x;
public:
  Foo() : x(0) {}
  Foo(int n) : x(n) {}
  int operator()() { return ++x; }
};

int main()
{
  Foo a;
  while (true) {
    int n = a();
    if (n > 10) break;
    cout << n << endl;
  }
}

クラス Foo は演算子 () を多重定義しているので、そのインスタンスは関数オブジェクトになります。main で Foo a; とインスタンスを生成すると、a() のように呼び出すことができます。この場合、メンバ変数 x をインクリメントしてから、その値を返します。実行結果は次のようになります。

$ clang++ sample146.cpp
$ ./a.out
1
2
3
4
5
6
7
8
9
10

●ジェネレータ

関数オブジェクトの応用例として、「ジェネレータ (generator)」というプログラムを取り上げます。ジェネレータは呼び出されるたびに新しい値を生成します。たとえば、Foo の関数オブジェクトは 1, 2, 3, ... のように呼び出すたびに +1 された数値を返します。つまり、整数列を発生する「ジェネレータ」と考えることができます。

ここでは簡単な例題として、IntVec の要素を順番に返すジェネレータを作ってみましょう。これは関数オブジェクトを使うと簡単に定義できます。次のリストを見てください。

リスト : ジェネレータ

class IntVec {
  //
  // 省略
  //

  // ジェネレータ
  class Generator {
    IntVec *vec;
    int idx;
  public:
    Generator(IntVec* v) : vec(v), idx(0) { }
    bool operator()(int& x) {
      if (idx >= vec->size) return false;
      x = vec->buff[idx++];
      return true;
    }
  };
  Generator make_gen() { return Generator(this); }
};

基本的な考え方はイテレータと同じです。内部クラス Generator の中で演算子 () を多重定義します。ベクタの要素は有限なので、データの有無を返す必要があります。ここでは返り値のデータ型を bool とし、要素は引数の参照型変数 x にセットして返すことにします。メンバ変数 idx がベクタの範囲を超えたときは false を返します。

簡単な実行例を示します。

リスト : ジェネレータ

void test3()
{
  IntVec a(10);
  int i = 1; 
  for (auto iter = a.begin(); iter < a.end(); ++iter)  
    *iter = i++;
  auto gen = a.make_gen();
  int x;
  while (gen(x)) cout << x << " ";
  cout << endl;
}
$ clang++ intvec.cpp
$ ./a.out

・・・略・・・

1 2 3 4 5 6 7 8 9 10

正常に動作していますね。


●プログラムリスト

//
// intvec.cpp : int 型ベクタクラス
//
//              Copyright (C) 2015-2023 Makoto Hiroi
//
#include <iostream>
#include <stdexcept>
using namespace std;

//
// int 型ベクタクラス
//
class IntVec {
  int *buff;
  size_t size;
public:
  explicit IntVec(size_t n) : buff(new int [n]), size(n) { }
  ~IntVec() { delete[] buff; }
  IntVec(const IntVec&);            // コピーコンストラクタ
  IntVec& operator=(const IntVec&); // 代入演算子
  int& operator[](int);             // 添字演算子
  // 出力演算子
  friend ostream& operator<<(ostream&, const IntVec&);
  // 高階関数
  void foreach(void (*func)(int)) {
    for (int i = 0; i < size; i++) func(buff[i]);
  }
  
  // イテレータ
  class Iterator {
    IntVec *vec;
    int idx;
  public:
    Iterator(IntVec* v, int n) : vec(v), idx(n) { }
    // 前置きの ++, -- 演算子
    Iterator& operator++() {
      if (idx < vec->size) idx++;
      return *this;
    }
    Iterator& operator--() {
      if (idx > 0) idx--;
      return *this;
    }
    // 間接参照
    int& operator*() { return vec->buff[idx]; }
    // 比較演算子
    bool operator==(const Iterator& iter) {
      return vec == iter.vec && idx == iter.idx;
    }
    bool operator!=(const Iterator& iter) {
      return vec != iter.vec || idx != iter.idx;
    }
    bool operator<(const Iterator& iter) {
      return vec == iter.vec && idx < iter.idx;
    }
    bool operator<=(const Iterator& iter) {
      return vec == iter.vec && idx <= iter.idx;
    }
    bool operator>(const Iterator& iter) {
      return vec == iter.vec && idx > iter.idx;
    }
    bool operator>=(const Iterator& iter) {
      return vec == iter.vec && idx >= iter.idx;
    }
  };

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

  // ジェネレータ
  class Generator {
    IntVec *vec;
    int idx;
  public:
    Generator(IntVec* v) : vec(v), idx(0) { }
    bool operator()(int& x) {
      if (idx >= vec->size) return false;
      x = vec->buff[idx++];
      return true;
    }
  };
  Generator make_gen() { return Generator(this); }
};

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

// 演算子の多重定義
// 代入
IntVec& IntVec::operator=(const IntVec& v)
{
  if (this != &v) {
    if (size != v.size) {
      delete[] buff;
      buff = new int [v.size];
      size = v.size;
    }
    for (int i = 0; i < size; i++) buff[i] = v.buff[i];
  }
  return *this;
}

// 添字
int& IntVec::operator[](int i)
{
  if (i < 0 || i >= size) throw out_of_range("IntVec: out of range");
  return buff[i];
}

// 出力
ostream& operator<<(ostream& output, const IntVec& v)
{
  output << "[";
  int i = 0;
  for (; i < v.size - 1; i++)
    output << v.buff[i] << ",";
  output << v.buff[i] << "]";
  return output;
}

void print(int x) { cout << x << endl; }

// 簡単なテスト
void test1()
{
  IntVec a(10);
  cout << a[0] << endl;
  cout << a[9] << endl;
  for (int i = 0; i < 10; i++) a[i] = i;
  cout << a << endl;
  IntVec b = a;
  cout << b << endl;
  b[0] = 10;
  b[9] = 20;
  cout << b << endl;
  cout << a << endl;
  IntVec c(5);
  c = a;
  c[0] = 100;
  cout << c << endl;
  cout << a << endl;
  IntVec d(15);
  d = a;
  d[0] = 200;
  cout << d << endl;
  cout << a << endl;
}

void test2()
{
  IntVec a(10);
  int i = 1;
  for (auto iter = a.begin(); iter < a.end(); ++iter)
    *iter = i++;
  for (auto iter = a.begin(); iter < a.end(); ++iter)
    cout << *iter << endl;
}

void test3()
{
  IntVec a(10);
  int i = 1;
  for (auto iter = a.begin(); iter < a.end(); ++iter)
    *iter = i++;
  auto gen = a.make_gen();
  int x;
  while (gen(x)) cout << x << " ";
  cout << endl;
}

int main()
{
  test1();
  test2();
  test3();
}

初版 2015 年 9 月 13 日
改訂 2023 年 4 月 9 日