仮想関数
まずは、C++ が備える仮想関数の挙動について確認しよう。仮想関数とは、クラスが持つオーバーライド可能なメンバ関数のことだ。
例
Human クラスは、仮想関数 Serif を持っている。
class Human
{
public:
virtual const char* Serif() const { return "I'm a Human." };
};
この Serif 関数はオーバーライドすることができる。
//------------------------------------------------------------
class John : public Human
{
public:
virtual const char* Serif() const override { return "I'm John." };
};
//------------------------------------------------------------
class Richie : public Human
{
public:
virtual const char* Serif() const override { return "I'm Richie." };
};
//------------------------------------------------------------
このようにオーバーライドされた関数は、Base クラス(ここでは Human クラス)をインターフェイスとして、次のように呼び分けることができる。
Human* pHuman = nullptr;
// John instance
pHuman = new John();
printf( "%s\n", pHuman->Serif() );
delete pHuman;
// Richie instance
pHuman = new Richie();
printf( "%s\n", pHuman->Serif() );
delete pHuman;
I'm John.
I'm Richie.
John 、Richie いずれのインスタンスも Human クラスのポインタを経由して仮想関数 Serif の呼び出しがなされる。しかし、実行されるのはオーバーライドされたそれぞれの Serif 関数であることがわかるだろう。このことを一般的にポリモーフィズム(多態性)と呼ぶ。
スケルトンクラス
ここからは、前述のポリモーフィズムによる一種の『分岐処理』を使って、機能の拡張性が高いクラスの設計方法を紹介しよう。
例題
犬を飼う人と、猫を飼う人の仕事内容をそれぞれ標準出力に印字したい。どちらも朝食と夕食を与える必要がある。加えて、犬は昼間に散歩を必要とすることがわかっている。
解決方法
犬を飼う人と猫を飼う人の仕事には共通点がある。それは朝食と夕食を与えるという仕事のようだ。しかし、犬を飼う人には、散歩をするという仕事が特別にある。つまり、「散歩をする」という仕事は、犬を飼う人ならば出力する必要があり、猫を飼う人ならば出力する必要がないということになる。
(方法1) if 文による分岐
まずはストレートに if 文を使って、分岐したい内容をそのままコードに書き起こしてみよう。
//---------------------------------------
enum class Animal
{
Dog,
Cat,
};
//---------------------------------------
class PetMaster
{
public:
void print_work()
{
feed_morning(); // 朝食
walk_daytime(); // 散歩
feed_evening(); // 夕食
}
public:
PetMaster( const Animal& animal )
: m_animal( animal ) {}
private:
void feed_morning()
{
printf( "朝ごはんですよ.\n" );
}
void walk_daytime()
{
if( m_animal == Animal::Dog ) {
printf( "散歩に行こう.\n" );
}
}
void feed_evening()
{
printf( "晩ごはんですよ.\n" );
}
private:
Animal m_animal;
};
//---------------------------------------
PetMaster クラスは、 enum によって外部から与えられるペットの種類に従って、内部で仕事内容を変える。
// DogMaster instance
printf( "--- DogMaster's work ---\n" );
PetMaster* pDogMaster = new PetMaster( Animal::Dog );
pDogMaster->print_work();
delete pDogMater;
// CatMaster instance
printf( "--- CatMaster's work ---\n" );
PetMaster* pCatMaster = new PetMaster( Animal::Cat );
pCatMaster->print_work();
delete pCatMaster;
--- DogMaster's work ---
朝ごはんですよ.
散歩に行こう.
晩ごはんですよ.
--- CatMaster's work ---
朝ごはんですよ.
晩ごはんですよ.
(方法2)スケルトンクラスによる分岐
まずは、PetMaster に実装する必要のあるすべての"仕事"関数を仮想関数で宣言し、クラスを定義する。
このとき、仮想関数の実装は必要がないため、純粋仮想関数で宣言する。ゆえに、このクラスをスケルトンクラスと呼ぶ。
//---------------------------------------
class PetMaster
{
public:
void print_work()
{
feed_morning(); // 朝食
walk_daytime(); // 散歩
feed_evening(); // 夕食
}
protected:
virtual void feed_morning() = 0;
virtual void walk_daytime() = 0;
virtual void feed_evening() = 0;
};
//---------------------------------------
次に、この仮想関数をオーバーライドすることで実装を与える。この実装を与えるクラスこそが『犬を飼う人』『猫を飼う人』を表現するクラスとなる。
//---------------------------------------
class DogMaster : public PetMaster
{
protected:
virtual void feed_morning() override { printf( "朝ごはんですよ.\n" ); }
virtual void walk_daytime() override { printf( "散歩に行こう.\n" ); }
virtual void feed_evening() override { printf( "晩ごはんですよ.\n" ); }
};
//---------------------------------------
class CatMaster : public PetMaster
{
protected:
virtual void feed_morning() override { printf( "朝ごはんですよ.\n" ); }
virtual void walk_daytime() override { /* do nothing */ }
virtual void feed_evening() override { printf( "晩ごはんですよ.\n" ); }
};
//---------------------------------------
継承先のクラスである DogMaster 、CatMaster クラスでそれぞれの"仕事"関数に実装が与えられた。ここで確認しておきたいところは CatMaster::walk_daytime 関数だ。この仮想関数はオーバーライドしているものの関数内部は空っぽにしてある。つまり、仮想関数自体は実行されるが、何の影響もないまま呼び出し元に帰ることになる。
PetMaster* pPetMaster = nullptr;
// DogMaster instance
printf( "--- DogMaster's work ---\n" );
pPetMaster = new DogMaster();
pPetMaster->print_work();
delete pPetMaster;
// CatMaster instance
printf( "--- CatMaster's work ---\n" );
pPetMaster = new CatMaster();
pPetMaster->print_work();
delete pPetMaster;
--- DogMaster's work ---
朝ごはんですよ.
散歩に行こう.
晩ごはんですよ.
--- CatMaster's work ---
朝ごはんですよ.
晩ごはんですよ.
方法2の優位性
方法1に比べて、定義の依存度が明らかに低いことがわかるだろう。インスタンスの生成において、enum による分岐を行っているにも関わらず、walk_daytime 関数内部でも if 文による分岐命令を描く必要がある。スケルトンクラスを定義することで、生成するインスタンスのクラスを切り替えるだけで実装の選択を行うことができる。また、新たな Animal の種類が増えたり、"仕事"関数を増減したい場合に、変更箇所を小さく抑えることができることもスケルトンクラスの魅力の1つだろう。
特徴を抽出するということ
プログラミングを行う上で最も重要で難しく、本質だと感じるのは、『物事の共通点や特徴を見極めること』。解決すべき問題を見つめ、その様相を読み解き、骨格を掘り出す。今回の紹介であれば、「何も必要としない /* do nothing */」こともまた特徴であるということだった。みなさんはコードを書くその一瞬前にどのようなことを感じ取り、意識的にまたは無意識的に何を見つけているのだろう。