仮想関数
多様性を実現する
継承は、あるクラスの機能を受け継いだ新しいクラスを定義することです。
前ページではごく単純に、ある基底クラスに新しい関数を追加しただけのシンプルな派生クラスを紹介しました。
これだけでもある程度便利に使うことはできますが、継承で重要なのは多様性(ポリモーフィズム)を実現できる点です。
そのためには仮想関数という機能が必要となります。
仮想関数
まずはサンプルコードを見てみましょう。
#include <iostream>
class Human
{
public:
//仮想関数
virtual void speak() const
{
std::cout << "Hello." << std::endl;
}
};
class Japanese : public Human
{
public:
//仮想関数をオーバーライド
void speak() const override
{
std::cout << "こんにちは。" << std::endl;
}
};
//引数を基底クラスの参照で受け取る
void action(const Human &human)
{
human.speak();
}
int main()
{
Human john;
Japanese taro;
action(john);
action(taro);
std::cin.get();
}
Humanクラスは挨拶を返す関数speakを持つシンプルなクラスです。
JapaneseクラスはHumanを継承し、日本語で挨拶をするクラスにしています。
ここで注目すべきは、どちらも同じ「speak」という名前で関数が定義されている点です。
さらに、Humanクラスのspeak関数には、先頭にvirtualというキーワードが追加されてます。
そして、Japaneseクラスのspeak関数の後ろにはoverrideというキーワードが追加されています。
実はこのコードはis-a関係になっていません。
is-a関係を忠実に守りたい場合は、後に説明する純粋仮想関数という機能を使います。
virtual
基底クラスの関数にvirtualを付けると、その関数は仮想関数というものになります。
仮想関数は基本的に普通の関数と同じですが、そのクラスを継承したとき、派生クラス側で機能が上書きされる(可能性がある)関数、という意味になります。
あくまでも可能性であって、必ず上書きされるわけではありませんし、必ず上書きして使わなければならないという意味ではありません。
また、基底クラスのインスタンスを生成して使う場合でも、仮想関数はそのまま普通の関数として使うことができます。
override
派生クラスの関数にoverrideを付けると、その関数は基底クラスの関数を上書きする関数という意味になります。
基底クラスの関数を上書きすることをオーバーライドといいます。
名前がオーバーロードと似ていますが別の機能なので注意してください。
オーバーロードは関数の引数が異っている必要がありますが、オーバーライドは引数や戻り値、constの有無などの関数定義(シグネチャという)がすべて一致していなければなりません。
(オーバーロードはC++の関数参照)
なお、overrideは比較的新しく追加されたキーワードです。
古いコンパイラでは基底クラスと同じくvirtualを使用します。
(virtualを記述する位置も基底クラスと同じです)
実は派生クラスではoverrideやvirtualを記述しなくてもオーバーライドは可能です。
しかしこれらのキーワードがないと、シグネチャ(引数などの定義)が異なる場合にオーバーライドにならず、新しい関数を定義したことになってしまいます。
これらのキーワードを付けていれば記述ミスなどがあればキチンとエラーになってくれますから、バグの混入を防ぐことができます。
ちなみに、基底クラスで仮想関数に指定されていない関数をオーバーライドしようとするとエラーになります。
多様性の実現
基底クラスは派生クラスのインスタンスを受け取れる
さて、サンプルコードではもう一つ重要な点があります。
それは「関数actionの引数はHumanクラスの参照型」という点です。
main関数ではHumanクラスとJapaneseクラスのインスタンスをひとつずつ生成しています。
それを関数actionにそのまま渡しています。
関数actionの引数はHumanクラスの参照型なのに、派生クラスであるJapaneseクラスのインスタンスを受け取ることができます。
そして、その結果としてそれぞれのクラスで定義されている通りのspeakクラスを呼び出すことができます。
上のサンプルコードの実行結果は以下になります。
Hello. こんにちは。
基底クラスと派生クラスではデータ型自体が異なるのでエラーになりそうなものですが、基底クラスは派生クラスのインスタンスを受け取れるという特徴があります。
派生クラスは、基底クラスを含むクラスです。
基底クラスに存在する関数は派生クラスにも存在することが保障されているため、基底クラスから派生クラスの関数も呼び出すことができるのです。
このように、呼び出し元からは同じ操作をしているのに、操作対象のオブジェクトによって違った結果を受け取れるのが多様性です。
ただし参照やポインタの時のみ
ただし、上のような呼び出し方ができるのはインスタンスを参照またはポインタとして受け取った場合のみです。
引数をインスタンスのコピーを受け取った場合は、引数で指定した通りのクラスの関数が呼び出されてしまい、多様性は発揮できません。
//引数をインスタンスのコピーで受け取る
void action(const Human human)
{
human.speak();
}
int main()
{
Human john;
Japanese taro;
action(john);
action(taro);
}
Hello. Hello.
これは、派生クラスのインスタンスを基底クラスとしてコピーした時、派生クラスで新たに追加したメンバ情報が失われてしまうためです。
これをスライシングといい、避けるべきとされています。
ちなみに、基底クラスのインスタンスを派生クラスとしてコピーすることは文法上できずエラーになります。
隠蔽
見た目がオーバーライドに似ているものに隠蔽というものがあります。
#include <iostream>
class BaseClass
{
public:
void print(int n) const
{
std::cout << n << std::endl;
}
};
class DerivedClass : public BaseClass
{
public:
//関数printを隠蔽
void print(const char *s) const
{
std::cout << s << std::endl;
}
};
int main()
{
BaseClass bc;
DerivedClass dc;
bc.print(10);
//dc.print(10); エラー
dc.print("abc");
std::cin.get();
}
派生クラスでは、基底クラスとに存在するメンバ関数と同じ名前のメンバ関数を定義しています。
なお、説明のためにあえて引数を変えていますが、引数などの定義を揃えても同じです。
このように記述すると、派生クラスのインスタンスからは基底クラスにある同名関数の存在が見えなくなり、アクセスができなくなります。
引数の種類を変えても同名関数のオーバーロードをしたことにはなりません。
もちろん、ポインタなどを使用してもアクセスできず、多様性はありません。
メンバ関数の隠蔽は、基底クラスの関数をそっくり置き換えてしまうことになります。
意図して隠蔽するのならば構いませんが、大抵は単なるミスです。
せっかく機能を継承しているのに多様性もない形で別物で置き換えるのは効率が悪く、設計自体を見直した方がいいかもしれません。
ちなみに、派生クラスのインスタンスからは、隠蔽された基底クラスのメンバ関数は使用できませんが、以下のようにすれば派生クラスのメンバ関数からは基底クラスの隠蔽されたメンバ関数にアクセスすることは可能です。
class BaseClass
{
public:
void print(int n) const
{
std::cout << n << std::endl;
}
};
class DerivedClass : public BaseClass
{
public:
//関数printを隠蔽
void print(const char *s) const
{
//隠蔽されたメンバ変数にアクセス
BaseClass::print(10);
std::cout << s << std::endl;
}
};
スコープ解決演算子を使用して「基底クラス名::メンバ関数」とすることで、名前空間を通してアクセスすることが可能になります。
仮想関数とnew演算子
基底クラスは、派生クラスのインスタンスをポインタまたは参照で扱うことができます。
そのため、以下のコードも有効です。
#include <iostream>
class BaseClass
{
};
class DerivedClass : public BaseClass
{
};
int main()
{
BaseClass *dc = new DerivedClass();
delete dc;
}
new演算子でインスタンスを生成すると、そのインスタンスのポインタが返ってきます。
ポインタですから、受け取る側の型が基底クラスであっても派生クラスのように扱うことができます。
ただし、この使い方は注意点があります。
派生クラスのメンバにアクセスできない
ポインタ変数dcの実体は派生クラスのインスタンスですが、基底クラスとして受け取っているため、基底クラスのメンバにしかアクセスできません。
#include <iostream>
class BaseClass
{
public:
void print()
{
std::cout << "BaseClass" << std::endl;
}
};
class DerivedClass : public BaseClass
{
public:
void printDerive()
{
std::cout << "DerivedClass" << std::endl;
}
};
int main()
{
BaseClass *dc = new DerivedClass();
//dc->printDerive(); エラー
delete dc;
std::cin.get();
}
派生クラスのメンバを使いたい場合は、素直に派生クラスとして受け取りましょう。
同名関数は仮想関数にする
これも本質は上で説明した「派生クラスのメンバにアクセスできない」と同じです。
派生クラスで同名関数を定義する場合はvirtualで仮想関数にしておかないと、正しい関数を呼び出すことができなくなります。
#include <iostream>
class BaseClass
{
public:
void print()
{
std::cout << "BaseClass" << std::endl;
}
virtual void printV()
{
std::cout << "BaseClassV" << std::endl;
}
};
class DerivedClass : public BaseClass
{
public:
void print()
{
std::cout << "DerivedClass" << std::endl;
}
void printV() override
{
std::cout << "DerivedClassV" << std::endl;
}
};
int main()
{
BaseClass *dc = new DerivedClass();
dc->print();
dc->printV();
delete dc;
std::cin.get();
}
BaseClass DerivedClassV
関数printは仮想関数ではない、通常の関数です。
これを呼び出すと、基底クラスのメンバ関数が呼び出されてしまっていまい、派生クラスのインスタンスなのに派生クラスのメンバ関数にアクセスできません。
仮想関数化した関数printVの方は、正しく派生クラスのメンバ関数を呼び出せています。
仮想デストラクタ
これはデストラクタの場合にも同様の事が言えます。
#include <iostream>
class BaseClass
{
public:
BaseClass()
{
std::cout << "BaseClassコンストラクタ" << std::endl;
}
~BaseClass()
{
std::cout << "BaseClassデストラクタ" << std::endl;
}
};
class DerivedClass : public BaseClass
{
public:
DerivedClass()
{
std::cout << "DerivedClassコンストラクタ" << std::endl;
}
~DerivedClass()
{
std::cout << "DerivedClassデストラクタ" << std::endl;
}
};
int main()
{
BaseClass *dc = new DerivedClass();
delete dc;
std::cin.get();
}
BaseClassコンストラクタ DerivedClassコンストラクタ BaseClassデストラクタ
インスタンスの生成自体は変数宣言の右辺である「new DerivedClass()」が行うので、派生クラスのコンストラクタにアクセスは可能です。
しかし、その後は基底クラスとして扱われるため、インスタンスの破棄時に派生クラスのデストラクタにアクセスができません。
これを解決するには、デストラクタも仮想関数にします。
#include <iostream>
class BaseClass
{
public:
BaseClass()
{
std::cout << "BaseClassコンストラクタ" << std::endl;
}
virtual ~BaseClass() //仮想デストラクタ
{
std::cout << "BaseClassデストラクタ" << std::endl;
}
};
class DerivedClass : public BaseClass
{
public:
DerivedClass()
{
std::cout << "DerivedClassコンストラクタ" << std::endl;
}
~DerivedClass() override //デストラクタのオーバーライド
{
std::cout << "DerivedClassデストラクタ" << std::endl;
}.
};
int main()
{
BaseClass *dc = new DerivedClass();
delete dc;
std::cin.get();
}
BaseClassコンストラクタ DerivedClassコンストラクタ DerivedClassデストラクタ BaseClassデストラクタ
デストラクタを仮想関数にすることを仮想デストラクタと言います。
デストラクタが呼ばれないと、コンストラクタで確保したリソース(メモリ領域など)が正しく破棄されないため、継承して使用するクラスのデストラクタはvirtualを付けておくことが推奨されます。
逆に言えば、デストラクタにvirtualが付いていないクラスは安易に継承すべきではないかもしれません。
なお、デストラクタは戻り値も引数もありませんから、派生クラス側ではoverrideやvirtualなどの修飾子を書かなくても問題は起こりません。
書いても問題ないので、オーバーライドしていることを明確にしておきたい場合には書いておきましょう。