C++編【言語解説】 第28章 継承と合成

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

この章の概要

この章の概要です。

合成

第26章第27章と続けて継承に関する話題を扱いましたが、 本章では、一旦継承から離れて、別の概念を説明した後、再び継承の話題に戻ります。

継承は強力な機能ではありますが、少々強力過ぎるという指摘もあります。 問題なのは、継承は、基底クラスに対する依存性が非常に高いことです。
派生クラスからは、基底クラスの「公開」と「限定公開」のメンバにアクセスできる訳ですから、 それらのメンバに依存するコードを書けるということになります。 そのため、派生クラスが作られた後では、基底クラスの「公開」や「限定公開」のメンバを変更することが容易では無くなる可能性があります。

そこで、もう少し緩やかな関係性を構築する手法が、合成(コンポジション、包含)です。 合成は、C++ の言語機能という訳ではなく、設計手法の領域になりますが、重要なので取り上げておきます。

合成は、あるクラスが別のクラス(のオブジェクト)を保持している、あるいは使用して実装しているという関係性です。 公開継承の関係性を is-a関係と呼んでいたように、 前者の場合を has-a関係(AはBを持っている)、 後者の場合を is-implemented-in-terms-of関係(AはBを使用して実装されている)と呼びます。 しかしながら、形としては特に変わりはありません。

実のところ、合成はこれまでにも、あまり意識せずに使っていると思います。 例えば、メンバ変数として std::string を使っていれば、 そのクラスは std::string のオブジェクトを使って実装されている訳ですから、合成の一種だと言えます。

class Student {
private:
	std::string  mName;
};

この場合、「Student は string を持っている」という関係性になります。 合成という概念は言ってしまえば、これだけのことです。

また、合成される側のオブジェクトが実体であっても、ポインタや参照であっても、合成の形にはなっており、同じことです。 ただし、ポインタや参照の場合は、その実体がどこか別のクラスや、クラスの外にあるかも知れず、 管理責任の有無で、違いがあるとも言えます。

class Student {
private:
	Pen*  mPen;
};

この場合、「Student は Pen を持っている」という関係性ですが、このクラスの他の部分の作りによっては、 この Pen の管理責任を Student自身が持っているのか、Student以外の誰かが持っているのか、分かれるところです。 例えば、そのペンが生徒の私物であるのなら前者でしょうし、貸し与えられたものなら後者かも知れません。


公開継承だと、基底クラスの「公開」「限定公開」のメンバへの依存性が生まれますが、 合成の場合は、合成されるクラスの「公開」メンバへの依存性のみが生まれます。 このように、合成の方が依存性を少なくできるので、一般的に、公開継承よりも合成を優先して使うべきとされています。

このような観点で考えると、相手クラスの「公開」「限定公開」「非公開」のすべてのメンバへアクセスできる、 フレンド関係(第25章)が、最も依存性が高い関係性であると言えます。勿論、最も避けるべき関係性です。

非公開継承 (private継承)

実は C++ では、合成を、継承の形で表現することが可能です。 これはかなり異質で、C++以外の言語ではあまり見かけない形です。

合成を継承の形で表現するには、非公開継承(private継承)を行います。
これまでに登場した、公開継承は、基底クラスを指定する際に publicキーワードを用いていましたが、 ここを private に変えるだけです。

class Base {};
class Derived : private Base {};

class の場合であれば、この場面での privateキーワードは省略可能で、同じ意味になります。 struct の場合は、省略すると「公開継承」になるので、同じ意味にはなりません。

class Base {};
class Derived1 : Base {};  // 非公開継承

struct Derived2 : Base {};  // 公開継承

非公開継承では、基底クラス側のすべてのメンバが、派生クラス側では「非公開」扱いになります。 従って、外部からは基底クラスのメンバにはアクセスできません(フレンドを除く)。
派生クラスからは、基底クラスの「公開」と「限定公開」のメンバにアクセスできます。 この点は、公開継承の場合と同様です。

何が OK で、何がエラーになるのか確認しておきましょう。

class Base {
public:
	void f1() {}

protected:
	void f2() {}

private:
	void f3() {}
};

class Derived : private Base {
public:
	void g()
	{
		f1();  // 「公開」は OK
		f2();  // 「限定公開」は OK
		f3();  // 「非公開」なのでエラー
	}
};

int main()
{
	Derived d;

	d.g();
	d.f1();  // 基底クラス側で「公開」だが、非公開継承なのでエラー
	d.f2();  // 基底クラス側で「限定公開」。当然、エラー
	d.f3();  // 基底クラス側で「非公開」。当然、エラー
}


通常の合成との大きな違いとして、以下の2点あります。

これらのいずれかの特性が必要になる場面では、非公開継承を活用できますが、 逆に、これらが必要なければ、普通の合成を使うべきです。
派生クラスからアクセスできる範囲は、公開継承でも非公開継承でも違いが無いので、 「合成」の項で説明した通り、依存性が強いことにも違いはありません。

非公開継承は、is-a関係にならないことを改めて注意して下さい。 意味としては合成に他ならないので、結ばれる関係性は has-a関係や is-implemented-in-terms-of関係になります。
実際、非公開継承では、派生クラスのオブジェクトを、基底クラスの型で扱うことができません

class Base {};
class Derived : private Base {};

void func(Base* b) {}

int main()
{
	Derived d;
	func(&d);  // エラー

	Base* b = new Derived();  // エラー
}

限定公開継承 (protected継承)

publicキーワードを使った公開継承、privateキーワードを使った非公開継承とくれば、 protectedキーワードを使った限定公開継承(protected継承)も存在します。 限定公開継承が一番使いどころが難しく、正直かなり分かりづらい機能です。

限定公開継承では、基底クラス側の「公開」のメンバが、派生クラス側では「限定公開」扱いになります。 それ以外のメンバのアクセス指定は、そのまま引き継がれます。 従って、外部からは基底クラスのメンバにはアクセスできません(フレンドを除く)。
派生クラスからは、基底クラスの「公開」と「限定公開」のメンバにアクセスできます。 この点は、どのタイプの継承でも同様です。

限定公開継承の最大のポイントは、 派生クラスのそのまた派生クラスからは、基底クラスの「公開」「限定公開」のメンバにアクセスできる点です。
非公開継承の場合、基底クラスの「限定公開」メンバは、派生クラスで「非公開」にされてしまうため、 更なる派生クラスを公開継承で作ったとしても、そのメンバはもう「非公開」なので、アクセスできませんが、 限定公開継承なら、「限定公開」のままなので、アクセスできるという訳です。
この複雑な特徴を言い換えると、外部からのアクセスを防ぎつつ(公開継承と異なる点)、 更なる派生クラスで「限定公開」メンバへアクセスすることを許す(非公開継承と異なる点)ということです。

class Base {
public:
	void f1() {}

protected:
	void f2() {}

private:
	void f3() {}
};

class Derived : protected Base {
public:
	void g()
	{
		f1();  // 「公開」は OK
		f2();  // 「限定公開」は OK
		f3();  // 「非公開」なのでエラー
	}
};

class Derived2 : public Derived {
public:
	void g2()
	{
		f1();  // Base の「公開」は、ここでは「限定公開」なので OK
		f2();  // Base の「限定公開」は、ここでも「限定公開」なので OK
		f3();  // Base の「非公開」は、ここでも「非公開」なのでエラー
	}
};

int main()
{
	Derived2 d2;

	d2.g2();
	d2.f1();  // 基底クラス側で「公開」だが、限定公開継承なのでエラー
	d2.f2();  // 基底クラス側で「限定公開」。当然、エラー
	d2.f3();  // 基底クラス側で「非公開」。当然、エラー
}

限定公開継承が構築する関係性は、非公開継承と同じく has-a関係や is-implemented-in-terms-of関係になります。 やはり、is-a関係では無いことに注意して下さい。

class Base {};
class Derived : protected Base {};

void func(Base* b) {}

int main()
{
	Derived d;
	func(&d);  // エラー

	Base* b = new Derived();  // エラー
}


練習問題

問題@ 第26章の練習問題で、 標準ライブラリの bitset(【標準ライブラリ】第13章)に、 すべてのビットが 1 になっているかどうかを判定するメンバ関数を、公開継承を使って追加するという題材を扱いました。 同様のことを、普通の合成、非公開継承、限定公開継承のそれぞれを使って行って下さい。


解答ページはこちら

参考リンク

更新履歴

'2016/4/2 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ