C++編【言語解説】 第29章 抽象クラスとインタフェースクラス

この章の概要

この章の概要です。

抽象クラス

第27章で解説した仮想関数を使うと、 処理の内容を派生クラスの側で書き換える(オーバーライド)ことができました。 これは便利な機能ですが、微妙な問題もあります。
それは、基底クラス側でも仮想関数の中身を用意しなければならないことです。 基底クラスが、それ単体でも意味があるのならば問題にはならないのですが、継承して使うことが前提のようなクラスの場合、 基底クラス側に何かしらの処理を記述することが難しくなります。

仕方が無いので、基底クラス側では assertabort、 例外(第31章)などでごまかすという手を取ることがありますが、 これらの方法では、実行時にならないと問題が検出できません。 できるだけ、コンパイルやリンクの時点で、問題を検出したいのです。

そこで、中身の無い仮想関数を作る機能が利用できます。 このような仮想関数を、純粋仮想関数と呼び、次のように使用します。

class Base {
private:
	virtual void func() = 0;
};

class Derived : public Base {
private:
	virtual void func() {}
};

純粋仮想関数は、virtualキーワードを使うことは仮想関数と同様ですが、 宣言の末尾に「= 0」を付ける点が異なります。 オーバーライドするときは「= 0」は付けません。

変な構文ですが、「中身が無い」=「ヌル (NULL)」と考えると、0 を使うことのニュアンスが分かるでしょうか? だからといって、NULL とか nullptr (C++11。第7章) を使うのは適切ではありません。

更に変な話に思えるかも知れませんが、純粋仮想関数の中身を書くことは、実は可能です。 それが必要になることはまず無いので取り上げませんが、 次の項で見る純粋仮想デストラクタは、たまに使う機会があります。

純粋仮想関数を含んだクラスを、抽象クラスと呼びます。 また、その対比として、それ以外のクラスを具象クラスと呼びます。 先ほどのサンプルプログラムで言えば、Base が抽象クラスで、Derived が具象クラスです。

抽象クラスは、インスタンス化することができません。 これは定義が無い関数(純粋仮想関数)が含まれていますから、クラスとして不完全な状態なので、当然と言えば当然ですが、 この特徴を利用すれば、継承して使用すること、および、純粋仮想関数の中身を派生クラス側で用意することを強制できる訳です。

int main()
{
	Base b1;                   // エラー。抽象クラスはインスタンス化できない
	Base* b2 = new Base();     // エラー。抽象クラスはインスタンス化できない
	Base* b3 = new Derived();  // OK。インスタンス化したのは具象クラスである
}


具体的な例として、第27章で登場した、Penクラスを改造してみます。

class Pen {
public:
	// 色を表す型
	// RGB(赤・緑・青)をそれぞれ 16進数2桁 (0x00〜0xff) で表し、
	// 0xff8000 のように指定する(この場合、R=0xff, G=0x80, B=0x00)
	typedef unsigned int Color_t;

public:
	explicit Pen(Color_t color) :
		mColor(color)
	{}

	virtual ~Pen()
	{}

	void DrawLine(int x1, int y1, int x2, int y2)
	{
		graphics::BeginDraw();
		DrawLineInner(x1, y1, x2, y2);
		graphics::EndDraw();
	}

protected:
	inline Color_t GetColor() const
	{
		return mColor;
	}

private:
	virtual void DrawLineInner(int x1, int y1, int x2, int y2) = 0;

private:
	Color_t  mColor;
};

class SolidPen : public Pen {
public:
	explicit SolidPen(Color_t color) :
		Pen(color)
	{}

protected:
	virtual void DrawLineInner(int x1, int y1, int x2, int y2)
	{
		// GetColor() の色を使って、(x1,y1) から (x2,y2) に向かって実線を描く
	}
};

class DotPen : public Pen {
public:
	explicit DotPen(Color_t color) :
		Pen(color)
	{}

protected:
	virtual void DrawLineInner(int x1, int y1, int x2, int y2)
	{
		// GetColor() の色を使って、(x1,y1) から (x2,y2) に向かって点線を描く
	}
};


int main()
{
//	Pen* pen1 = new Pen(0x000000);
	Pen* pen2 = new SolidPen(0x000000);
	Pen* pen3 = new DotPen(0x000000);

//	pen1->DrawLine(0, 0, 50, 50);
	pen2->DrawLine(20, 0, 70, 50);
	pen3->DrawLine(40, 0, 90, 50);
}

やりたいことは、Pen というクラスを抽象的(空想的、想像上のもの、など表現は何でも構いませんが、とにかく実在しないもの)な存在にするということです。 そのためには、第27章のプログラムの Penクラスのように、 実線を描く具体的なコードが含まれているのはおかしいと考えることもできます。
つまり、「Pen とは、(線種を問わず)直線を描くことができる仮想的な存在である」と考えたいので、 線種に具体性を持たせたくない訳です。 そこで、直線を描く処理にあたる DrawLineInnerメンバ関数を、純粋仮想関数にして、具体的な部分を派生クラスに任せます。

純粋仮想デストラクタ

稀な話ではありますが、抽象クラスにしたいが、純粋仮想関数にできるメンバ関数が1つも無いという状況があり得ます。 その場合は、無理に仮想関数をひねり出すよりも、デストラクタを純粋仮想にするのが得策です。 このようなデストラクタは、純粋仮想デストラクタと呼ばれます。

宣言の方法は、通常のメンバ関数を純粋仮想関数にする場合と同様で、virtualキーワードと、末尾の「= 0」を付けます。 ただし、純粋仮想デストラクタは、中身は空でも良いので、定義を書かなければなりません

class Base {
public:
	virtual ~Base() = 0;
};

Base::~Base()
{
}

ちなみに、次のように一緒に書いてしまうことはできません。

class Base {
public:
	// エラー
	virtual ~Base() = 0
	{}
};

インタフェースクラス

純粋仮想関数だけで構成されたクラスを、インタフェースクラス(インタフェース)と呼ぶことがあります。 特別、C++ にそのような機能がある訳ではなく、抽象クラスの特別な形ということです。

インタフェースクラスは、抽象クラスから純粋仮想関数以外のメンバ関数を廃し、メンバ変数も無いものですが、 更に言い換えれば、一切の定義を持たず、宣言だけで構成されているクラスとも考えられます。 つまり、具体的な定義は派生クラスに任されており、 共通の型を提供するためだけに存在するのが、インタフェースクラスです

抽象クラスの場合だと、具体的な中身のあるメンバ関数を持つことができるので、 派生クラスで共通して使いたい処理を、「限定公開」の形で用意しておけば、派生クラスの実装は簡単になります。
また、メンバ変数を持っているので、これもやはり、派生クラスで共通で使いたい情報を置いておくことができます。 (メンバ変数の場合は、「限定公開」ではなくて、常に「非公開」にすべきであることは注意して下さい)。

このような、抽象クラスとの違いによって、当然、使い道にも違いが生まれます。
抽象クラスは、公開継承して使うのであれば、結局のところ is-a関係を構築する機能です。 一方、インタフェースクラスは、can-do関係と呼ばれる関係性を構築します。 これは「Derived can X」の関係性で、X にはメンバ関数の名前が入ると考えて下さい。 つまり、インタフェースクラスは、そこから派生したクラスが「何ができるか」を表現するものです。


抽象クラスのところで取り上げた Penクラスの例を、インタフェースクラスに置き換えて考えてみましょう。
インタフェースクラスの場合、「何ができるか」という考え方なので、そもそも「Pen」という物体の名前を使うのはふさわしくありません。 「何ができるか」=>「直線を描ける」というのであれば、LineDrawer のような名称の方がそれらしいでしょう。 そのままインタフェースクラスの名前として採用してもいいですが、よくある命名規約として、 インタフェースクラスの名前の先頭に、Interface を表す「I」を付けるというものがあります。 当サイトでも、これを採用して「ILineDrawer」と命名します。

class ILineDrawer {
public:
	// 色を表す型
	// RGB(赤・緑・青)をそれぞれ 16進数2桁 (0x00〜0xff) で表し、
	// 0xff8000 のように指定する(この場合、R=0xff, G=0x80, B=0x00)
	typedef unsigned int Color_t;

public:
	virtual ~ILineDrawer()
	{}

	virtual void DrawLine(int x1, int y1, int x2, int y2) = 0;
};

class SolidPen : public ILineDrawer {
public:
	explicit SolidPen(Color_t color) :
		mColor(color)
	{
	}

	virtual void DrawLine(int x1, int y1, int x2, int y2)
	{
		graphics::BeginDraw();
		// mColor の色を使って、(x1,y1) から (x2,y2) に向かって実線を描く
		graphics::EndDraw();
	}

private:
	Color_t  mColor;
};

class DotPen : public ILineDrawer {
public:
	explicit DotPen(Color_t color) :
		mColor(color)
	{}

	virtual void DrawLine(int x1, int y1, int x2, int y2)
	{
		graphics::BeginDraw();
		// mColor の色を使って、(x1,y1) から (x2,y2) に向かって点線を描く
		graphics::EndDraw();
	}

private:
	Color_t  mColor;
};


int main()
{
//	ILineDrawer* pen1 = new ILineDrawer(0x000000);
	ILineDrawer* pen2 = new SolidPen(0x000000);
	ILineDrawer* pen3 = new DotPen(0x000000);

//	pen1->DrawLine(0, 0, 50, 50);
	pen2->DrawLine(20, 0, 70, 50);
	pen3->DrawLine(40, 0, 90, 50);
}

インタフェースクラスになったことで、メンバ変数 mColor は、それぞれの派生クラスに移動されました。 これは、コードが重複するようになってしまったという見方もできますし、 一方で、色に関しての裁量が派生クラスに任されたことで自由度が増したとも言えます。 例えば、色を複数持ったペンを作ることも容易にできます。

抽象クラスからインタフェースクラスに変わったため、もはや is-a関係ではなくなりました。 そのため、「ペン」である必要性も無くなったことは注目すべき点です。 ペンでも、筆でも、コンピュータのペイントソフトの機能でも、校庭に線を引くライン引きでも、 とにかく「直線を描ける」のなら何でも、ILineDrawerインタフェースクラスから派生することができます。

インタフェースクラスに変わっても、そこから派生クラスを定義する方法として、「公開継承」を選択したのなら、 インタフェースクラス型のポインタや参照を使って、派生クラスのインスタンスを扱えます。


練習問題

問題@ 「飛ぶ (Fly)」という動作に注目して、「鳥 (Bird)」のクラスを定義して下さい。

問題A 「飛ぶ (Fly)」という動作は、鳥だけでなく、飛行機などにも適用できます。 そのような観点で、インタフェースクラスを設計して下さい。


解答ページはこちら

参考リンク

更新履歴

'2016/4/9 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ