純粋仮想デストラクタの必要性

C++でなんでそんなもんが必要なのか、と疑問に思ってたけど、解決したのでメモ。
純粋仮想関数って何というところから純粋仮想デストラクタの必要な場面を考えてみた。

純粋仮想関数とは

抽象クラスを定義するときに、抽象クラスから派生したクラスが何であれ、その関数だけは実装しないとダメというのを示すもの。

純粋仮想関数を使う(例:電気製品)

抽象クラスを継承して派生クラスを作りたい場合を考える。
例えば、何らかの処理をしなければならないが、抽象クラスでそれを定義してはいけない場合がある。
例えば電気製品には"電源を入れる(power_on)"という動作がある、と定義する。
だが、実際に電源を入れて何が行われるかは、それは派生クラスで定義するべきだろう。
電気製品である、テレビとエアコンに電源を入れる動作を実装するとこうなる。
抽象クラスにするには、クラスが持つ関数どれか1つでも純粋仮想関数として定義する事である。

#include <iostream>

using namespace std;

// 電気製品を抽象化した基底クラス
class AbstractElectronics{
public:
	virtual void power_on() = 0; // 電気製品は電源を入れることができる
	virtual ~AbstractElectronics();
};
AbstractElectronics::~AbstractElectronics(){} // デストラクタの動作を定義


// テレビを示す派生クラス
class Television : public AbstractElectronics{
public:
	void power_on(){ cout << "Television Power ON !!" << endl; }
};

// エアコンを示す派生クラス
class AirConditioner : public AbstractElectronics{
public:
	void power_on(){ cout << "AirConditioner Power ON !!" << endl; }
};


// 動作確認
int main(){
//	AbstractElectronics *electronic = new AbstractElectronics(); // 電気製品 <- 間違い
	AbstractElectronics *tv = new Television(); // 電気製品のテレビ
	AbstractElectronics *aircon = new AirConditioner(); // 電気製品のエアコン
	
	tv->power_on(); // 電気製品は電源を入れる事ができる
	aircon->power_on();
	
	delete tv; // テレビを廃棄処分
	delete aircon; // テレビを廃棄処分
	return 0;
}

電気製品はElectronicsで示せるが、抽象なクラスを強調するため、AbstractElectronicsとした。
至って普通の継承の形になったと思う。
AbstractElectronics *electronic = new AbstractElectronics();
が成功しないのは、AbstractElectronicsにはpower_onという操作が具体的に定義されていないから。

純粋仮想デストラクタが必要な場合(例:刃物)

さて、ここから少し意地悪に考えてみる。
手順が全て決まったモノになっていたらどうだろう。
例えば刃物を考える。
刃物は"切る(cut)"という動作が出来る。よく切れるナイフも包丁でも同じだろう。
しかし、刃物と呼ばれるモノはあっても、刃物という実体は存在しない。
刃物という実体は、ここではナイフや包丁を指す。
なので、刃物とは抽象化したものだといえる。
コレに則ってこんな実装をしてみる。

class AbstractCutlery{
public:
	virtual void cut(){ cout << "Cut" << endl; } // デフォルトの動作
	virtual ~AbstractCutlery();
};
AbstractCutlery::~AbstractCutlery(){}


class Knife : public AbstractCutlery{}; // 実装するモノは特に無い
class Kitchenknife : public AbstractCutlery{};

// 動作確認
int main(){
	AbstractCutlery * cutlery = new AbstractCutlery();
	AbstractCutlery * knife = new Knife();
	AbstractCutlery * kitchen_knife = new Kitchenknife();
	
	cutlery->cut();
	knife->cut();
	kitchen_knife->cut();
	
	delete cutlery;
	delete knife;
	delete kitchen_knife;
	
	return 0;
}

Cutleryでもいいのだが、AbstractCutleryと抽象なクラスを強調するためにAbstractを含めた。
刃物のデフォルトな動作である"切る(cut)"は抽象クラスで既に具体的に定義されている。
だが、ソースコードをみての通り、このままでは抽象クラスのはずのAbstractCutleryまで生成できてしまう。
純粋仮想関数がなくなっているので、抽象クラスとしての機能をしていない状態である。
この動作は本意ではない。


そこで純粋仮想デストラクタである。
変更は容易で、抽象クラスAbstractCutleryのデストラクタを以下のように変更する

virtual ~AbstractCutlery() = 0;

これで純粋仮想デストラクタを定義した事になる。
純粋仮想関数が存在する事をコンパイラに教える事によって、そのクラスは抽象クラスという事を示す事ができる。
また、抽象クラスを継承した派生クラスでは、純粋仮想関数で定義された操作を実装しなければならない。
デストラクタは全オブジェクトで必ず定義する必要があるので、抽象クラスにしてしまうのであればデストラクタを純粋仮想関数にするというのも適していると言える。

まとめ

今回の刃物と電気製品の例の違いは、
デフォルトとしての動作を持たせているかいないかという点で、
刃物は何であれ切る事が出来るが、刃物という実体が無いものでは切る事はできないので、"刃物"という実体がないものを生成できないようにしたいが為、純粋仮想関数をデストラクタに適用した。
電気製品は電源を入れることが出来るが、その実装は電気製品によって依存するので、実装は各派生クラスに必ず定義させるため、純粋仮想関数を電源を入れる動作に適用した。


ちなみに、電気製品の例でAbstractElectronics::power_onを純粋仮想関数に定義しているが、
さらに抽象クラスでAbstractElectronics::power_onを具体的に実装した場合、
派生クラスで、継承元の関数を呼び出す事が出来るし、
power_onが純粋仮想関数なのでAbstractElectronicsの実体を生成する事も抑制できる。

	
// 電気製品を抽象化した基底クラス
class AbstractElectronics{
public:
	virtual void power_on() = 0; // 電気製品は電源を入れることができる
	virtual ~AbstractElectronics();
};
void AbstractElectronics::power_on(){cout << "AbstractElectronics Power ON !!" << endl; } // 電気製品としてのpower_onの操作を定義してみる
AbstractElectronics::~AbstractElectronics(){} // デストラクタの動作を定義


// テレビを示す派生クラス
class Television : public AbstractElectronics{
public:
	void power_on(){ AbstractElectronics::power_on(); cout << "Television Power ON !!" << endl; }
};

// エアコンを示す派生クラス
class AirConditioner : public AbstractElectronics{
public:
	void power_on(){ AbstractElectronics::power_on(); cout << "AirConditioner Power ON !!" << endl;  }
};


// 動作確認
int main(){
//	AbstractElectronics *electronic = new AbstractElectronics(); // 相変わらず生成できない
	AbstractElectronics *tv = new Television(); // 電気製品のテレビ
	AbstractElectronics *aircon = new AirConditioner(); // 電気製品のエアコン
	
	tv->power_on(); // 電気製品は電源を入れる事ができる
	aircon->power_on();
	
	delete tv; // テレビを廃棄処分
	delete aircon; // テレビを廃棄処分
	return 0;
}

まとめのまとめ

  • 抽象クラスを導入するなら、純粋仮想関数を作る
  • 純粋仮想関数は派生クラスに実装を任せるという意味ではない。派生クラスにその操作を持たせる事を強制しているだけである。(なので純粋仮想関数でも実装は出来る)
  • 抽象クラスだが純粋仮想関数にすべき操作が無い場合に純粋仮想デストラクタの出番


僕のオブジェクト指向の認識が間違っていたらこの記事は破綻しかねないがry