M.Hiroi's Home Page

Linux Programming

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

[ PrevPage | C++ | NextPage ]

例外処理

一般に、「例外 (exception)」はエラー処理で使われる機能です。「例外=エラー処理」と考えてもらってもかまいません。最近は例外処理をサポートしているプログラミング言語が多くなりました。もちろん、C++にも例外処理があります。なお、エラーが発生したことを「例外が発生した」とか「例外が送出された」という場合もあります。本ページでもエラーのことを例外と記述することにします。

●例外の捕捉

通常、例外が発生するとC++はプログラムの実行を中止 (異常終了) しますが、致命的な例外でなければプログラムの実行を継続する、または特別な処理を行わせたい場合もあるでしょう。このような場合、例外処理がとても役に立ちます。

C++の例外処理は try, catch, throw を使います。try に続くブロック { } 内で例外が送出された場合、そのあとに続く catch でその例外を捕捉することができます。例外を送出するには throw を使います。

try 文の構文を下図に示します。

try {
  処理A;
}
catch (データ型 引数) {
  処理B;
}

図 : 例外処理

try 文は、そのあとに定義されている処理 A を実行します。処理 A が正常に終了した場合は try 文も終了します。もしも、処理 A で例外が発生した場合、処理 A の実行は中断され、その例外が catch 節で指定した例外と一致すれば、その catch 節を実行します。

catch ( ) { } を例外ハンドラ (exception handller) と呼びます。例外ハンドラは必ず try { } の直後に定義してください。例外ハンドラは複数定義することができます。catch のあとの ( ) には、例外として送出されるデータ型やクラスを指定します。なお、指定するデータ型はポインタや参照型でもかまいません。

●例外の送出

例外は throw で送出することができます。

throw 式;

throw が実行されるとプログラムの実行を直ちに中断して、例外を受け止める catch 節を探索します。見つけた場合はそこに制御を移します。このとき、throw で指定した式の評価結果が catch 節の変数に渡されます。該当する catch 節が見つからない場合、標準ライブラリ関数 terminate を呼び出してプログラムを異常終了します。

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

リスト : 例外処理の使用例 (sample140.cpp)

#include <iostream>
using namespace std;

class Foo { };
class Bar { };

int foo(int a)
{
  switch(a) {
  case 0:
    throw 0;
  case 1:
    throw 1.2345;
  case 2:
    throw "oops!";
  case 3:
    throw Foo();
  default:
    throw Bar();
  }
}

void bar(int a)
{
  try {
    foo(a);
  }
  catch (int e) {
    cerr << "Error: " << e << endl;
  }
  catch (double e) {
    cerr << "Error: " << e << endl;
  }
  catch (char const* e) {
    cerr << "Error: " << e << endl;
  }
  catch (Foo e) {
    cerr << "Error: Foo" << endl;
  }
}

int main()
{
  for (int i = 0; i < 5; i++)
    bar(i);
}
$ clang++ sample140.cpp
$ ./a.out
Error: 0
Error: 1.2345
Error: oops!
Error: Foo
terminate called after throwing an instance of 'Bar'
中止

関数 foo で例外を送出し、関数 bar で例外を捕捉します。C++の例外は、try 文の中で呼び出した関数の中で例外が送出されても、関数の呼び出し履歴 (コールスタック) を遡って catch 節を探索し、該当する例外を捕捉することができます。

throw はどんなデータでも例外として送出することができます。throw 0 は catch 節の int e で捕捉することができます。このとき、変数 e の値は 0 になります。同様に、throw 1.2345 は double e で、throw "oops!" は char const* e で、throw Foo() は Foo e で捕捉することができます。なお、catch 節 で Foo& e のように参照型にしても、throw Foo() を捕捉することができます。

最後に、foo で throw Bar() を実行すると例外が送出されますが、bar では Bar のインスタンスを受け取る catch 節が定義されていません。この場合、例外は捕捉されずにプログラムは異常終了します。

なお、catch (...) { } のようにデータ型に ... を指定すると、あらゆる例外を捕捉することができます。このとき、引数を指定することはできません。それから、catch 節の中で引数なしの throw を実行すると、同じ例外を再送出することができます。

●大域脱出

例外処理を使うと、評価中の関数からほかの関数へ制御を移す「大域脱出 (global exit)」を実現することができます。

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

リスト : 大域脱出 (sample141.cpp)

#include <iostream>
using namespace std;

class GlobalExit {};

void bar1()
{
  cout << "call bar1\n";
}

void bar2()
{
  cout << "call bar2\n";
  throw GlobalExit();
}

void bar3()
{
  cout << "call bar3\n";
}

void foo()
{
  bar1(); bar2(); bar3();
}

int main()
{
  try {
    foo();
  }
  catch (GlobalExit e) {
    cout << "Global Exit\n";
  }
}

try 文で関数 foo() を実行すると、次のようになります。

$ clang++ sample141.cpp
$ ./a.out
call bar1
call bar2
Global Exit

実行の様子を下図に示します。

通常の関数呼び出しは、呼び出し元の関数に制御が戻ります。ところが bar2() で throw が実行されると、呼び出し元の関数 foo() を飛び越えて、制御が try 文の catch 節に移るのです。このように、例外処理を使って関数を飛び越えて制御を移すことができます。

なお、大域脱出はとても強力な機能ですが、多用すると処理の流れがわからなくなる、いわゆる「スパゲッティプログラム」になってしまいます。例外処理はあくまでもエラーを処理するために使ったほうがよいでしょう。

●例外クラス

C++の場合、throw はどんなデータ型でも例外として送出することができますが、通常は標準ライブラリ <stdexcept> に定義されている例外クラス logic_error または runtime_error の派生クラスを使用します。これらの例外クラスは <excpetion> に定義されている例外クラス exception を継承しています。例外クラスは次に示すような階層構造になっています。

参考文献 1 によると、『logic_error は、原則として、プログラムが実行を開始する前か、関数、コンストラクタの引数をテストしたときに見付かるエラーであり、runtime_error はその他すべてのエラーである。』 とのことです。

logic_error と rumtime_error のコンストラクタは文字列を受け取ることができます。その内容はメンバ関数 what で表示することができます。簡単な例を示しましょう。

リスト : 例外クラスの使用例 (sample142.cpp)

#include <iostream>
#include <stdexcept>
using namespace std;

void foo(int n)
{
  switch (n) {
  case 0:
    throw runtime_error("foo runtime error");
  case 1:
    throw logic_error("foo logic error");
  default:
    throw "oops!";
  }
}

int main()
{
  for (int i = 0; i < 3; i++) {
    try {
      foo(i);
    }
    catch (const exception& e) {
      cout << e.what() << endl;
    }
    catch (...) {
      cout << "other error\n";
    }
  }
}
$ clang++ sample142.cpp
$ ./a.out
foo runtime error
foo logic error
other error

関数 foo で例外を送出して、main で例外を捕捉します。logic_error と runtime_error は exception を継承しているので、どちらの例外も exception& e で捕捉することができます。C++の場合、例外クラスは "const 例外クラス&" のように参照型で受け取るのが一般的なようです。throw "oops!" は文字列なので exception を継承していませんが、次の catch 節はすべてのエラーを補足するので、ここで送出された文字列 "oops!" が捕捉されます。

●例外クラスの定義

例外クラスは他の例外クラスを継承することで、ユーザーが独自に定義することができます。通常は logic_error か runtime_error を継承するといいでしょう。次の例を見てください。

リスト : 例外クラスの定義 (sample143.cpp)

#include <iostream>
#include <stdexcept>
using namespace std;

class foo_error : public runtime_error {
public:
  foo_error(const string& s) : runtime_error(s) { }
};
  
void foo(int n)
{
  switch (n) {
  case 0:
    throw runtime_error("foo runtime error");
  case 1:
    throw logic_error("foo logic error");
  default:
    throw foo_error("foo error oops!");
  }
}

int main()
{
  for (int i = 0; i < 3; i++) {
    try {
      foo(i);
    }
    catch (const exception& e) {
      cout << e.what() << endl;
    }
    catch (...) {
      cout << "other error\n";
    }
  }
}
$ clang++ sample143.cpp
mhiroi@DESKTOP-FQK6237:~/cpp$ ./a.out
foo runtime error
foo logic error
foo error oops!

このように、foo_error は runtime_error を継承しているので、メンバ変数やメンバ関数を定義しなくても適当なコンストラクタを用意するだけで動作します。

●例外処理とデストラクタ

局所変数 (スタック領域) に確保されたインスタンスは、関数の実行が終了するとき (スコープの範囲外になったとき) デストラクタが呼び出されますが、例外処理によって関数の実行が中断された場合でもデストラクタが呼び出されます。次のリストを見てください。

リスト : 例外処理とデストラクタ (sample144.cpp)

#include <iostream>
using namespace std;

class GlobalExit {};

class Foo {
  int x;
public:
  Foo() : x(0) {}
  Foo(int n) : x(n) {}
  ~Foo() { cout << "Foo " << x << " destroy\n"; }
};

void bar1()
{
  cout << "call bar1\n";
}

void bar2()
{
  cout << "call bar2\n";
  throw GlobalExit();
}

void bar3()
{
  cout << "call bar3\n";
}

void foo()
{
  Foo a(1);
  Foo* b = new Foo(2);
  bar1();
  bar2();
  bar3();
  delete b;
}

int main()
{
  try {
    foo();
  }
  catch (GlobalExit e) {
    cout << "Global Exit\n";
  }
}
$ clang++ sample144.cpp
$ ./a.out
call bar1
call bar2
Foo 1 destroy
Global Exit

関数 foo でクラス Foo のインスタンスを局所変数 a とポインタ変数 b にセットします。関数の実行が正常に終了するならば、delete b で b のインスタンスが破棄され、そのあとで a のインスタンスが破棄されます。ところが、bar2 で throw が実行されると、foo を跳び越えて制御が main の catch 節に移ります。この場合でも、局所変数 a のデストラクタが呼び出されます。

ところが、ポインタ変数 b はヒープ領域からメモリを取得しているので、明示的に delete しない限りデストラクタは呼び出されません。この場合、例外が発生するとメモリリークが生じることになります。

メモリリークだけではなく、プログラムの途中で例外が送出されると、残りのプログラムは実行されないため、必要な処理が行われない場合もあります。他のプログラミング言語、たとえば Java や Python では、try 文を終了するときに必ず実行する処理を記述する機能 (finally 節) があります。このような機能はC++にはありませんが、クラスのデストラクタで代用することが可能です。

クラスを作るのが面倒であれば、標準ライブラリを使うことを考えてみてください。たとえば、配列を動的に取得したい場合は vector クラスを使う、ポインタ変数はスマートポインタ (auto_ptr など) に格納して使う、といった方法があります。標準ライブラリの使い方は回を改めて説明する予定です。


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

ベクタクラスの作成

今回は簡単な例題として、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 日

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

[ PrevPage | C++ | NextPage ]