M.Hiroi's Home Page

Linux Programming

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

[ PrevPage | C++ | NextPage ]

多重継承

今回は「多重継承」について説明します。実をいうと、M.Hiroi は多重継承に対してあまりいいイメージを持っていません。私見ですが、多重継承はメリットよりもプログラムを複雑にするデメリットの方が大きいのではないか、と思っています。とくに、下図のクラス A, B, C, E のような菱形の関係をC++でプログラムする場合、複雑な問題を引き起こすことが知られています。

多重継承は強力な機能ですが万能ではありません。多重継承は慎重に扱うべきだと思っています。それでは最初に、多重継承の基本的な使い方について簡単に説明します。

●多重継承の使い方

多重継承は次に示すように class 文で基底クラスを複数指定するだけです。

class name : public super_class_name1,
             public super_class_name2,
             ...
             public super_class_nameN {
  ...
}

単一継承と同じく、継承修飾子には public を指定してください。なお、同じクラスを複数指定することはできません。簡単な例題として、Foo と Bar の 2 つのクラスを継承するクラス Baz を考えてみましょう。次の図を見てください。

クラス Foo にはメンバ変数 a とメンバ関数 get_a, put_a が定義されています。同様に、クラス Bar にはメンバ変数 b とメンバ関数 get_b, put_b が定義されています。次に、Foo と Bar を多重継承したクラス Baz を定義します。Baz にはメンバ変数 c とメンバ関数 get_c, put_c を定義します。Foo, Bar, Baz のインスタンスを生成すると、上図に示したように、Baz のインスタンスには Foo と Bar で定義されたメンバ変数 a, b も含まれます。このように、複数の基底クラスのメンバ変数が Baz に継承されます。

クラス Baz にはメンバ関数 get_c, put_c しか定義されていませんが、クラス Foo と Bar を継承することにより、get_a, put_a, get_b, put_b を呼び出すことができます。Baz のインスタンスに対して get_a を呼び出すと、クラス Baz には get_a が定義されていないのでスーパークラス Foo を調べ、そこで定義されている get_a が呼び出されます。もちろん、取り出される値は Baz のインスタンスにある変数 a の値です。同様に、メンバ関数 get_b も呼び出すことができます。このように、Foo と Bar のメンバ関数が Baz に継承されます。

これをプログラムすると次のようになります。

リスト : 多重継承の使用例 (sample3601.cpp)

#include <iostream>
using namespace std;

class Foo {
  int a;
public:
  Foo() : a(0) {}
  Foo(int x) : a(x) {}
  int get_a() const { return a; }
  void put_a(int x) { a = x; }
};

class Bar {
  int b;
public:
  Bar() : b(0) {}
  Bar(int x) : b(x) {}
  int get_b() const { return b; }
  void put_b(int x) { b = x; }
};

class Baz : public Foo, public Bar {
  int c;
public:
  Baz(int x, int y, int z) : Foo(x), Bar(y), c(z) { }
  int get_c() const { return c; }
  void put_c(int x) { c = x; }
};

int main()
{
  Baz* a = new Baz(1, 2, 3);
  cout << a->get_a() << endl;
  cout << a->get_b() << endl;
  cout << a->get_c() << endl;  
  a->put_a(10);
  a->put_b(20);
  a->put_c(30);
  cout << a->get_a() << endl;
  cout << a->get_b() << endl;
  cout << a->get_c() << endl;
  Foo* b = a;
  b->put_a(100);
  cout << b->get_a() << endl;
  Bar* c = a;
  c->put_b(200);
  cout << c->get_b() << endl;
}

クラス Baz の定義で、コロンの後ろに public Foo と public Bar を指定します。これで Foo と Bar を継承することができます。Baz のコンストラクタでは、初期化リストでスーパークラスの Foo と Bar のコンストラクタを呼び出します。さっそく実行してみましょう。

$ clang++ sample3601.cpp
$ ./a.out
1
2
3
10
20
30
100
200

継承したメンバ関数 get_a, put_a, get_b, put_b を呼び出すことができるのは当然です。それから、Baz のインスタンスは基底クラス Foo* 型の変数 b や Bar* 型の変数 c にセットすることができます。この場合、インスタンスのデータ型は Foo* (または Bar*) になるので、変数 b から呼び出せるメンバ関数は get_a, put_a だけ、変数 c から呼び出せるメンバ関数 get_b, put_b だけになります。

●メンバ変数の衝突

クラスを多重継承する場合、異なる基底クラスで同じ名前のメンバ変数やメンバ関数が定義されていることがあります。C++の場合、基底クラスに同じ名前のメンバ変数があっても、それを一つにまとめることはしません。たとえば、Foo と Bar にメンバ変数 a があり、それを多重継承してクラス Baz を作成すると、インスタンスには Foo に対応するメンバ変数 a と Bar に対応するメンバ変数 a の領域が確保されます。次の図を見てください。

上図のように、Bar のインスタンスには変数 a の領域が 2 つあります。変数 a にアクセスする場合、変数名 a だけではどちらの領域にアクセスしたらよいのかコンパイラが判断できずにエラーとなります。他のオブジェクト指向言語、たとえば CLOS (Common Lisp Object Sysytem) や Python などでは、同じ名前のメンバ変数はインスタンスに一つしか存在しないので、曖昧さを生じることはありません。ただし、そのことで他の問題が発生することがあります。

たとえば、Foo にはメンバ関数 method_a があり、Bar にはメンバ関数 method_b があるとしましょう。この 2 つのメンバ関数はまったく異なる働きをします。ここで、method_a はメンバ変数 x を使っていて、method_b もメンバ変数 x を使っていると、CLOS や Python などの多重継承では問題が発生します。

Foo と Bar を多重継承して Baz を作成した場合、Baz のインスタンスにはメンバ変数 x がひとつしかありません。method_a と method_b はひとつしかないメンバ変数 x を使うことになります。この場合、どちらかのメソッドは正常に動作しないでしょう。これでは多重継承する意味がありませんね。

C++の場合、同じ名前のメンバ変数があっても、基底クラスごとにメンバ変数のメモリ領域が確保されるので、このような問題が発生することはありません。ただし、メンバ変数にアクセスする場合は、スコープ解決演算子 (::) を使って曖昧さを解決する必要があります。

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

リスト : メンバ変数の衝突 (sample3602.cpp)

#include <iostream>
using namespace std;

struct Foo {
  int a;
  Foo(int x) : a(x) {}
};

struct Bar {
  int a;
  Bar(int x) : a(x) {}
};

struct Baz : public Foo, public Bar {
  Baz(int x, int y) : Foo(x), Bar(y) {}
};

int main()
{
  Baz a(10, 20);
  cout << a.Foo::a << endl;
  cout << a.Bar::a << endl;
}
$ clang++ sample3602.cpp
$ ./a.out
10
20

メンバ変数名の前に クラス名:: を付加すれば、そのクラスのメンバ変数にアクセスすることができます。

●メンバ関数の衝突

多重継承する基底クラスで同じ名前のメンバ関数が定義されている場合、メンバ変数と同じように、どのクラスのメンバ関数を呼び出すか明示しないとコンパイルエラーになります。CLOS や Python などでは、継承関係からメンバ関数 (メソッド) の優先順位を決定することができるので、C++のような曖昧さが生じることはありません。

ただし、優先順位を決定するアルゴリズムはちょっと複雑です。拙作のページ Python 入門第 6 回CLOS 入門: 多重継承 で簡単に説明しているので、興味のある方はお読みくださいませ。

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

リスト : メンバ関数の衝突 (sample3603.cpp)

#include <iostream>
using namespace std;

struct Foo {
  int a;
  Foo(int x) : a(x) {}
  void print() { cout << "Foo " << a << endl; }
};

struct Bar {
  int a;
  Bar(int x) : a(x) {}
  void print() { cout << "Bar " << a << endl; }  
};

struct Baz : public Foo, public Bar {
  Baz(int x, int y) : Foo(x), Bar(y) {}
};

int main()
{
  Baz a(10, 20);
  a.Foo::print();
  a.Bar::print();
  // a.print();  // コンパイルエラー
}
$ clang++ sample3603.cpp
$ ./a.out
Foo 10
Bar 20

Foo と Bar には同じ名前のメンバ関数 print が定義されています。Baz のインスタンスから print を呼び出す場合、どちらのクラスのメンバ関数を呼び出すか、スコープ解決演算子を使って指定します。もちろん、Baz でメンバ関数 print をオーバーライドすることもできます。このとき、print を仮想関数にすると、ポリモーフィズムを働かせることができます。次の例を見てください。

リスト : メンバ関数のオーバーライド (sample3604.cpp)

#include <iostream>
using namespace std;

struct Foo {
  int a;
  Foo(int x) : a(x) {}
  virtual void print() { cout << "Foo " << a << endl; }
};

struct Bar {
  int a;
  Bar(int x) : a(x) {}
  virtual void print() { cout << "Bar " << a << endl; }  
};

struct Baz : public Foo, public Bar {
  Baz(int x, int y) : Foo(x), Bar(y) {}
  void print() {
    Foo::print();
    Bar::print();
  }
};

int main()
{
  Baz* a = new Baz(10, 20);
  a->print();
  Foo* b = a;
  b->print();
  Bar* c = a;
  c->print();
}
$ clang++ sample3604.cpp
$ ./a.out
Foo 10
Bar 20
Foo 10
Bar 20
Foo 10
Bar 20

Baz のインスタンスは Foo* 型や Bar* 型の変数 b, c にセットすることができます。そして、print が仮想関数なので、b や c から print を呼び出すと、Baz でオーバーライドした print が実行されます。

●仮想基底クラス

次は同じ基底クラス Foo を継承しているクラス Bar1 と Bar2 を多重継承することを考えてみましょう。下図を見てください。

Bar1 と Bar2 は Foo を単一継承しています。Baz は Bar1 と Bar2 を多重継承します。この場合、C++ではクラス Foo を 2 つ継承することになります。実際、上図のように Baz のインスタンスには Foo のメンバ変数 a のメモリ領域が 2 つ確保されます。Foo のメンバ変数 a にアクセスするときは、Bar1 と Bar2 のどちらのメンバ変数なのか明示する必要があります。

なお CLOS や Python では、このような場合でも Baz のインスタンスに確保されるメンバ変数 a の領域は一つしかありません。

これをプログラムすると次のようになります。

リスト : 同じクラスを間接的に継承する (sample3605.cpp)

#include <iostream>
using namespace std;

struct Foo {
  int a;
  Foo(int x) : a(x) {}
};

struct Bar1 : public Foo {
  int b;
  Bar1(int x, int y) : Foo(x), b(y) {}
};

struct Bar2 : public Foo {
  int c;
  Bar2(int x, int y) : Foo(x), c(y) {}
};

struct Baz : public Bar1, public Bar2 {
  int d;
  Baz(int x, int y, int z) : Bar1(x, y), Bar2(y, z), d(z) {}
};

int main()
{
  Baz a(10, 20, 30);
  cout << a.Bar1::a << endl;
  cout << a.Bar2::a << endl;
  cout << a.b << endl;
  cout << a.c << endl;
  cout << a.d << endl;
}
$ clang++ sample3605.cpp
$ ./a.out
10
20
20
30
30

Bar1::a とすると、Bar1 が継承した Foo のメンバ変数 a に、Bar2::a とすると、Bar2 が継承した Foo のメンバ変数 a にアクセスすることができます。なお、Foo のメンバ関数の呼び出しも同様です。

クラス Baz からみると、基底クラス Foo は Bar1 と Bar2 のどちらかひとつあれば機能するはずです。また、そのようにクラスを一つにまとめたほうが曖昧さがなくなるので便利です。C++の場合、クラスを継承するときに virtual を指定すると、同じ基底クラスを一つにまとめてくれます。これを「仮想基底クラス」といいます。

class Foo { ... };
class Bar1 : virtual public Foo { ... };
class Bar2 : virtual public Foo { ... };
class Baz : public Bar1, public Bar2 { ... };

これで Baz のインスタンスで確保されるメンバ変数 a の領域は一つになり、クラス Foo, Bar1, Bar2, Baz は菱形の継承関係になります。

ここで、仮想基底クラスのコンストラクタは今までの方法とは異なり、一番下の派生クラスのコンストラクタの初期化リスト、この場合は Baz のコンストラクタにおいて、一番最初に呼び出されることに注意してください。Baz で Foo のコンストラクタの呼び出しが明示されていない場合はデフォルトコンストラクタが呼び出されます。

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

リスト : 仮想基底クラスの使用例 (sample3606.cpp)

#include <iostream>
using namespace std;

struct Foo {
  int a;
  Foo() : a(0) { cout << "Foo " << a << endl; }
  Foo(int x) : a(x) { cout << "Foo " << a << endl; }
};

struct Bar1 : virtual public Foo {
  int b;
  Bar1(int x, int y) : Foo(x), b(y) { cout << "Bar1 "<< b << endl; }
};

struct Bar2 : virtual public Foo {
  int c;
  Bar2(int x, int y) : Foo(x), c(y) { cout << "Bar2 " << c << endl; }
};

struct Baz : public Bar1, public Bar2 {
  int d;
  Baz(int x, int y, int z) : Bar1(x, y), Bar2(y, z), d(z) {
    cout << "Baz " << d << endl;
  }
};

int main()
{
  Baz a(10, 20, 30);
  cout << a.a << endl;
  cout << a.b << endl;
  cout << a.c << endl;
  cout << a.d << endl;
  Bar1 b(1, 2);
  cout << b.a << endl;
  cout << b.b << endl;
  Bar2 c(3, 4);
  cout << c.a << endl;
  cout << c.c << endl;
}
$ clang++ sample3606.cpp
$ ./a.out
Foo 0
Bar1 20
Bar2 30
Baz 30
0
20
30
30
Foo 1
Bar1 2
1
2
Foo 3
Bar2 4
3
4

Baz のコンストラクタの初期化リストで Foo のコンストラクタを呼び出していないので、デフォルトコンストラクタが一番最初に呼び出されていることがわかります。Bar1 と Bar2 のコンストラクタから Foo のコンストラクタが呼び出されることはありません。Bar1 と Bar2 のインスタンスを生成するときは、今までと同様の方法でコンストラクタが呼び出されます。

もしも、Baz を継承して新しいクラスを定義する場合、Foo のコンストラクタは新しいクラスのコンストラクタから呼び出されることになります。このように、継承関係によって仮想基底クラスの初期化のタイミングが異なるので、仮想基底クラスは引数無しのコンストラクタで初期化する、もしくは、仮想基底クラスではメンバ変数を定義しないほうがよいとされています。

●菱形継承とメンバ関数

このように、仮想基底クラスのコンストラクタは、最初に 1 回だけ呼び出されることがC++の仕様で保証されていますが、同様なことを他のメンバ関数で行うことは簡単ではありません。次の例を見てください。

リスト : 仮想基底クラスのメンバ関数を呼び出す (sample3607.cpp)

#include <iostream>
using namespace std;

class Foo {
public:
  virtual void method_a() { cout << "Foo::method_a\n"; }
};

class Bar1 : virtual public Foo {
public:
  void method_a() {
    Foo::method_a();
    cout << "Bar1::method_a\n";
  }
};

class Bar2 : virtual public Foo {
public:
  void method_a() {
    Foo::method_a();
    cout << "Bar2::method_a\n";
  }
};

class Baz : public Bar1, public Bar2 {
public:
  void method_a() {
    Bar1::method_a();
    Bar2::method_a();
  }
};

int main()
{
  Baz a;
  a.method_a();
}
$ clang++ sample3607.cpp
$ ./a.out
Foo::method_a
Bar1::method_a
Foo::method_a
Bar2::method_a

メンバ関数 method_a は、呼び出すとき最初に 1 回だけ Foo のメンバ関数 method_a を実行しなければならないものとします。Bar1, Bar2 の場合は簡単です。method_a の中で Foo の method_a を最初に呼び出すだけです。ところが Baz の場合、Bar1, Bar2 のメソッドを順番に呼び出すと、Foo の method_a を 2 回呼び出してしまいます。

他のプログラミング言語、たとえば CLOS や Python ではメソッドの優先順位を決定することができます。CLOS の場合は Baz -> Bar1 -> Bar2 -> Foo となります。そして、CLOS では優先順位に従って基底クラスのメソッドを簡単に呼び出すことができるようになっています。CLOS の場合、関数 call-next-method を使います。なお、メソッド名の前に super を指定するプログラミング言語もあります。

Baz::method_a で call-next-method を呼び出すと Bar1::method_a が呼び出されます。その中で call-next-method を呼び出すと Bar2::method_a が呼び出され、さらにその中で call-next-method を呼び出すと Foo::method_a が呼び出されます。このように、CLOS では基底クラスのメンバ関数を簡単に呼び出すことができます。

C++の場合、メンバ関数の呼び出しに優先順位はなく、プログラマが指定しなくてはなりません。最初に 1 回だけ Foo::method_a を呼び出したいならば、Bar1, Bar2 固有の処理を行うメンバ関数を用意して、Foo の method_a を呼び出す処理と分離する必要があります。次のリストを見てください。

リスト : 仮想基底クラスのメンバ関数を呼び出す (sample3608.cpp)

#include <iostream>
using namespace std;

class Foo {
public:
  virtual void method_a() { cout << "Foo::method_a\n"; }
};

class Bar1 : virtual public Foo {
public:
  void method_owner_a() {
    cout << "Bar1::method_a\n";
  }
  void method_a() {
    Foo::method_a();
    method_owner_a();
  }
};

class Bar2 : virtual public Foo {
public:
  void method_owner_a() {
    cout << "Bar2::method_a\n";
  }
  void method_a() {
    Foo::method_a();
    method_owner_a();
  }
};

class Baz : public Bar1, public Bar2 {
public:
  void method_a() {
    Foo::method_a();
    Bar1::method_owner_a();
    Bar2::method_owner_a();
  }
};

int main()
{
  Baz a;
  a.method_a();
}
$ clang++ sample3608.cpp
$ ./a.out
Foo::method_a
Bar1::method_a
Bar2::method_a

Bar1, Bar2 にメンバ関数 method_owner_a を用意します。Baz の method_a では最初に Foo::method_a を呼び出して、それから Bar1::method_owner_a と Bar2::method_owner_a を呼び出します。これで仮想基底クラス Foo の method_a を最初に 1 回だけ呼び出すことができます。

●菱形継承におけるメンバ関数の曖昧さ

菱形継承にすると、曖昧だったメンバ関数の呼び出しが、そうでなくなる場合があります。次のリストを見てください。

リスト : 菱形継承でのメンバ関数の呼び出し (sample3609.cpp)

#include <iostream>
using namespace std;

class Foo {
public:
  virtual void method_a() { cout << "Foo::method_a\n"; }
};

class Bar1 : virtual public Foo {
public:
  void method_a() { cout << "Bar1::method_a\n"; }
};

class Bar2 : virtual public Foo { };

class Baz : public Bar1, public Bar2 { };

int main()
{
  Baz a;
  a.method_a();
}
$ clang++ sample3609.cpp
$ ./a.out
Bar1::method_a

Baz から見ると、method_a は Bar1 のそれと、Bar2 を経由して Foo1 の method_a の 2 つがあります。この場合、method_a の呼び出しは曖昧になると思われるでしょうが、実は Bar1 の method_a が呼び出されます。

菱形継承にしない場合、method_a の呼び出しは Foo と Bar1 の二つがあるので曖昧になるのですが、菱形継承にすると Bar1 と Bar2 の基底クラス Foo は同じものになります。この場合、Foo の method_a は Bar1 でオーバーライドしているので、Baz から method_a を呼び出すと、オーバーライドした Bar1 の method_a が実行されます。

CLOS などメソッドに優先順位をつけるプログラミング言語を経験したことがあると、これは当然の動作のように思うのですが、メンバ関数に優先順位をつけないC++では、仕様に反しているように思えて、なかなか理解できないかもしれません。

●Mix-in

今まで説明したように、C++の多重継承は複雑で難解です。もっとも、多重継承が複雑になるのはC++に限った話ではなく、他のプログラミング言語でも似たような状況になりがちです。たとえば、単一継承の場合、クラスの階層は木構造になりますが、多重継承ではグラフになります。木構造の場合、クラスの優先順位は簡単にわかりますが、グラフになると優先順位を理解するのは難しくなります。

これを軽減するために、メンバ変数 (属性) を継承する基底クラスはひとつだけに限定して、あとの基底クラスはメソッド (実装) だけを継承するという方法があります。この方法を Mix-in といいます。

具体的には、メンバ変数を定義せずにメソッドだけを記述したクラスを用意します。属性の継承は単一継承になりますが、実装のみを記述したクラスはいくつ継承してかまいません。ひとつのクラスに複数の実装を混ぜることから Mix-in と呼ばれています。

なお、Mix-in は特別な機能ではなく、多重継承を使いこなすための方法論にすぎません。多重継承を扱うことができるプログラミング言語であれば Mix-in を行うことが可能です。ちなみに、この Mix-in という方法を言語仕様に取り込んだのが Ruby です。

Mix-in のイメージを下図に示します。


        図 : Mix-in

クラス C はクラス B を継承していて、そこにクラス Mixin A が Mix-in されています。クラス D もクラス B を継承していますが、Mix-in されているクラスは Mixin B となります。

多重継承の問題点は Mix-in ですべて解決できるわけではありませんが、クラスの階層構造がすっきりとしてわかりやすくなることは間違いありません。Mix-in は多重継承を使いこなす優れた方法ではないかと思っています。

●Enumerable

それでは Mix-in の例題として、クラス Enumerable を作ってみましょう。Enumerable はコンテナクラスに高階関数を Mix-in します。これは Ruby のモジュール (Mix-in 用のクラス) Enumerable を参考にしました。追加するメンバ関数を下表に示します。

表 : Enumerable<T> のメンバ関数
名前機能
bool member(F func)func が真となる要素を探す
int position(F func)func が真となる要素の位置を返す
int count(F func)func が真となる要素の個数を返す
vector<T> mapcar(F func)要素に func を適用した結果を vector に格納して返す
vector<T> filter(F func)func が真となる要素を vector に格納して返す
U fold(F func, U init)すべての要素を func を用いて結合した結果を返す

これらのメンバ関数はすべて関数テンプレートで定義します。T は Enumerable のテンプレート仮引数で、コンテナクラスが保持している要素のデータ型を表します。F は関数オブジェクトを表すテンプレート仮引数で、U は T 以外のデータ型を表すテンプレート仮引数です。C++の場合、クラステンプレートの中でも関数テンプレートを定義することができます。プログラムは次のようになります。

リスト : Mix-in 用のクラス Enumerable

template<class T>
class Enumerable {
public:
  virtual bool enum_empty() = 0;
  virtual T& enum_get() = 0;
  virtual void enum_next() = 0;
  virtual void enum_init() = 0;

  template<class F>
  bool member(F func) {
    enum_init();
    while (!enum_empty()) {
      if (func(enum_get())) return true;
      enum_next();
    }
    return false;
  }

  template<class F>
  int position(F func) {
    int i = 0;
    enum_init();
    while (!enum_empty()) {
      if (func(enum_get())) return i;
      i++;
      enum_next();
    }
    return -1;
  }

  template<class F>
  int count(F func) {
    int c = 0;
    enum_init();
    while (!enum_empty()) {
      if (func(enum_get())) c++;
      enum_next();
    }
    return c;
  }

  template<class F>
  vector<T> mapcar(F func) {
    vector<T> a;
    enum_init();
    while (!enum_empty()) {
      a.push_back(func(enum_get()));
      enum_next();
    }
    return a;
  }

  template<class F>
  vector<T> filter(F func) {
    vector<T> a;
    enum_init();
    while (!enum_empty()) {
      if (func(enum_get())) a.push_back(enum_get());
      enum_next();
    }
    return a;
  }

  template<class F, class U>
  U fold(F func, U init) {
    U a = init;
    enum_init();
    while (!enum_empty()) {
      a = func(a, enum_get());
      enum_next();
    }
    return a;
  }
};

Enumerable は Mix-in を前提としているのでメンバ変数の定義は不要です。コンテナクラスの要素は純粋仮想関数 enum_init, enum_get, enum_next, enum_empty を使ってアクセスします。enum_init はイテレータの begin に、enum_get は * 演算子、enum_next は ++ 演算子に相当します。enum_empty は取り出す要素がなくなったら真を返します。

これらの関数は Mix-in するクラスで定義するものとします。つまり、4 つの関数を定義さえすれば、どんなクラスでも Enumberable を Mix-in することができるわけです。たとえば、vector に Enumerable を Mix-in してみましょう。次のリストを見てください。

リスト : Mix-in の例題

template<class T>
class EnumVec : public vector<T>, public Enumerable<T> {
  typename vector<T>::iterator iter;
public:
  void enum_init() { iter = this->begin(); }
  T& enum_get() { return *iter; }
  void enum_next() { ++iter; }
  bool enum_empty() { return iter == this->end(); }
};

今回は簡単な例題なので、デフォルトコンストラクタ以外のコンストラクタは省略します。メンバ変数 iter は継承した vector のイテレータを保持します。typename は vector<T>::iterator がデータ型であることをコンパイラに認識させるために必要です。typename を省略するとコンパイルエラーになります。

enum_init は this->begin() の返り値を iter にセットします。これで iter は vector の先頭要素を指し示します。enum_get は return で *iter を返すだけです。enum_next は ++ 演算子で iter を一つ進めます。enum_empty は iter と this->end() を比較するだけです。

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

リスト : 簡単なテスト (sample3610.cpp)

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

//
// ・・・省略・・・
//

int main()
{
  EnumVec<int> a;
  for (int i = 0; i < 8; i++) a.push_back(i);
  cout << a.member([](int x) { return x == 0; }) << endl;
  cout << a.member([](int x) { return x == 7; }) << endl;
  cout << a.member([](int x) { return x == 8; }) << endl;
  cout << a.position([](int x) { return x == 0; }) << endl;
  cout << a.position([](int x) { return x == 7; }) << endl;
  cout << a.position([](int x) { return x == 8; }) << endl;
  cout << a.count([](int x) { return x == 0; }) << endl;
  cout << a.count([](int x) { return x == 7; }) << endl;
  cout << a.count([](int x) { return x == 8; }) << endl;
  vector<int> b = a.mapcar([](int x) { return x * x; });
  for (int x : b) cout << x << " ";
  cout << endl;
  vector<int> c = a.filter([](int x) { return x % 2 == 0; });
  for (int x : c) cout << x << " ";
  cout << endl;
  cout << a.fold([](int a, int x) { return a + x;}, 0) << endl;
}
$ clang++ sample3610.cpp
$ ./a.out
1
1
0
0
7
-1
1
1
0
0 1 4 9 16 25 36 49
0 2 4 6
28

正常に動作していますね。複数のクラスで共通の操作 (メンバ関数) を定義したい場合、Mix-in はとても役に立ちます。

ところで、C++は高機能なプログラミング言語なので、このほかにも Mix-in を実現する方法があるようです。興味のある方は 参考 URL をお読みくださいませ。

●参考 URL

  1. C++ で ruby 風 Mix-in, (みねこあさん)

初版 2015 年 11 月 29 日
改訂 2023 年 4 月 15 日

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

[ PrevPage | C++ | NextPage ]