C++編【言語解説】 第30章 多重継承

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

多重継承

これまでの章では、1つのクラスから継承を行って、派生クラスを定義していましたが、 2つ以上のクラスから継承することも可能です。 このような、複数のクラスから継承を行うことを、多重継承と呼びます。 また、1つのクラスからの継承を、多重継承と対比させて、単一継承と呼びます。
多重継承を行うと、複数ある基底クラスのそれぞれのメンバが、派生クラスに引き継がれます。

多重継承の構文は、以下のようになります。

class Base1 {};
class Base2 {};
class Derived : public Base1, public Base2 {};

このように、「,」で区切って、複数の基底クラスを指定します。 それぞれの継承に指定するアクセス指定子は、混在しても問題ありません。

class Base1 {};
class Base2 {};
class Derived : public Base1, private Base2 {};

この場合、Base1 を「公開継承」、Base2 を「非公開継承」します。

多重継承のクラス構造

多重継承を使って、次のような構造を作ると、厄介な問題を引き起こすことがあります。

class Ancestor {};
class Base1 : public Ancestor {};
class Base2 : public Ancestor {};
class Derived : public Base1, public Base2 {};

つまり、多重継承の基底になる2つのクラス(3つ以上でも同様)が、1つの共通クラスから派生している構造です。 この継承構造を図にすると、次のようになります。

多重継承

ポイントは、Ancestor が2カ所に存在することです。

このような構造になるということは、 Derivedクラスをインスタンス化すると、Ancestor は2つ出来るということであり、ここに問題が潜んでいます。 つまり、Derivedクラスのオブジェクトから、Ancestorクラスのメンバへアクセスしようとすると、 「どの Ancestor なのか?」が曖昧になってしまう訳です。

どのような形で曖昧さが問題になるのか、見ていきましょう。 まず、Ancestorクラスにメンバ関数がある場合を考えてみます。

class Ancestor {
public:
	void f1() {}
	virtual void f2() {}
};

class Base1 : public Ancestor {
public:
	virtual void f2() {}
};
class Base2 : public Ancestor {};
class Derived : public Base1, public Base2 {};

int main()
{
	Derived* d = new Derived();
	d->f1();
	d->f2();
	delete d;
}

このプログラムはコンパイルに失敗します。 問題なのは、f1、f2 の呼び出しがともに曖昧であることです。 f1 は Ancestorクラスでしか定義されていませんが、Derived から見ると、 Base1 の基底としての Ancestor と、Base2 の基底としての Ancestor が存在しているため、 どちらの f1 を呼び出そうとしているのか判断できません。

曖昧さを無くすには、Ancestorクラスのメンバへのアクセスが、Base1経由で行うべきものなのか、 Base2経由で行うべきものなのかを明示する必要があります。 そのための方法としては、以下の2つがあります。

static_cast<Base1*>(d)->f1();
d->Base2::f1();

1つは、キャストを使って、ポインタ(あるいは参照)を基底クラスを指すように変換することです。 この方法で解決できるということは、一旦、基底クラスのポインタ型(あるいは参照型)の変数を経由させるのでも構いません。 基底クラス型への変換は暗黙的に行えるので、この方法ならば static_cast も不要です。
もう1つの方法は、スコープ解決演算子(::) を使って、基底クラスの名前を明示することです。 やや見慣れない構文ですが、これも有効です。

f1 に関していえば、Ancestorクラスでしか定義されていないので、いちいち曖昧さを解決させられるのは少々不服に感じますが、 f2 は仮想関数であり、Base2 だけでオーバーライドされていますから、曖昧さの解決は重大な意味を持ちます。


今度は、Ancestorクラスがメンバ変数を持っている場合を考えてみましょう。
静的でないメンバ変数は、オブジェクト1つごとにメモリ領域を確保しなければなりませんから、 Ancestor を2つ含む形になると、メモリも2カ所に確保されることになります。 メモリの使用量が増えることが問題という訳ではなく、 Base1経由で見たメンバ変数と、Base2経由で見たメンバ変数が別物であるという点が問題になり得ます。

静的メンバ変数の場合は、クラスごとに1つのメモリ領域を取るので、重複することはありません。

確認してみましょう。

#include <iostream>

class Ancestor {
public:
	Ancestor() : mValue(0)
	{}

	inline void Print() const
	{
		std::cout << mValue << std::endl;
	}

protected:
	inline void SetValue(int value)
	{
		mValue = value;
	}

private:
	int mValue;
};

class Base1 : public Ancestor {
public:
	inline void Set(int value)
	{
		SetValue(value);
	}
};

class Base2 : public Ancestor {
public:
	inline void Set(int value)
	{
		SetValue(value);
	}
};

class Derived : public Base1, public Base2 {};

int main()
{
	Derived* d = new Derived();
	d->Base1::Set(100);
	d->Base2::Set(200);
	d->Base1::Print();
	d->Base2::Print();
	delete d;
}

実行結果:

100
200

Base1 経由で設定した値と、Base2 経由で設定した値とが、別物として扱われていることが分かると思います。

仮想継承

多重継承の際に、共通の基底クラスを、本当にただ1つの実体として持ちたい場合には、 仮想継承を用います。 仮想継承を行うには、基底クラスを指定する際に virtualキーワードを付加します。

class Ancestor {};
class Base1 : public virtual Ancestor {};
class Base2 : public virtual Ancestor {};
class Derived : public Base1, public Base2 {};

こうすると、継承構造は次の図のように、菱形(ダイアモンド形)になります。

多重継承

このように、Base1 経由でみた Ancestor と、Base2 経由でみた Ancestor は同一のものになりました。 なおこのとき、Ancestor は、仮想基底クラスと呼ばれます。 前の項で試したプログラムを、仮想継承に変更して再度実行して確認してみます。

#include <iostream>

class Ancestor {
public:
	Ancestor() : mValue(0)
	{}

	inline void Print() const
	{
		std::cout << mValue << std::endl;
	}

protected:
	inline void SetValue(int value)
	{
		mValue = value;
	}

private:
	int mValue;
};

class Base1 : public virtual Ancestor {
public:
	inline void Set(int value)
	{
		SetValue(value);
	}
};

class Base2 : public virtual Ancestor {
public:
	inline void Set(int value)
	{
		SetValue(value);
	}
};

class Derived : public Base1, public Base2 {};

int main()
{
	Derived* d = new Derived();
	d->Base1::Set(100);
	d->Base2::Set(200);
	d->Base1::Print();
	d->Base2::Print();
	delete d;
}

実行結果:

200
200

後から設定した方の値 200 しか、出力されていないことが分かります。

ところで、Ancestor が1つになったということは、Printメンバ関数の呼び出しについては、曖昧さが無くなったはずです。 実際、次のように呼び出すことが出来ます。

d->Print();


練習問題

問題@ 「飛べるもの」を表す抽象クラス FlyObject と、「乗り物」を表す抽象クラス Vehicle があるとき、 「飛行機」のクラスを多重継承を使って定義して下さい。 FlyObject と Vehicle はそれぞれ、速度を表すメンバ変数 mSpeed と、それを返す GetSpeedメンバ関数を持つものとします。

問題A 問題@において、FlyObjectクラスを廃止して、代わりに「飛べるもの」を表すインタフェースクラス IFly を導入したら、 派生クラスはどう変化するでしょうか。 IFlyインタフェースクラスは、「飛ぶ」という動作にしか興味が無いので、Fly という純粋仮想関数と仮想デストラクタのみを持ちます。


解答ページはこちら

参考リンク

更新履歴

'2016/5/2 新規作成。



前の章へ

次の章へ

C++編のトップページへ

Programming Place Plus のトップページへ