この記事では、C++における「仮想関数」(virtual function)について解説します。
仮想関数は、オブジェクト指向プログラミングにおける多態性(ポリモーフィズム)を実現するための重要な機能ですあり、仮想関数を使用すると、派生クラスで基本クラスの関数をオーバーライドし、動的に関数の呼び出しを決定できます。
仮想関数とは
仮想関数は、基底クラス(親クラス)において宣言される関数で、派生クラス(子クラス)でオーバーライドされることを前提とした関数です。
仮想関数を使うと、基底クラスのポインタや参照を通じて派生クラスのオーバーライドされた関数を呼び出すことが可能になります。
仮想関数を使うことによるメリット・デメリット
メリット
- 多態性(ポリモーフィズム)の実現
-
仮想関数を使うことで、基底クラスのポインタや参照を通じて派生クラスの異なる実装を動的に呼び出すことができます。これにより、コードの柔軟性と再利用性が向上します。たとえば、異なる種類のオブジェクトを同じインターフェースで扱うことができるようになります。
- インターフェースの統一
-
仮想関数を使うことで、共通のインターフェースを提供し、異なるクラスが同じ操作を実行することができます。これにより、コードの可読性と保守性が向上します。
- 遅延バインディング
-
仮想関数は、どの関数を実行するかが実行時に決定されます。これにより、ランタイムの状況に応じて適切な処理を選択できるため、動的な振る舞いを実現できます。
- プラグインや拡張の容易さ
-
仮想関数を使うことで、新しいクラスを追加して既存のシステムに容易に組み込むことができます。プラグインシステムなどで、後から機能を拡張するのに役立ちます。
- 派生クラスの動的操作
-
基底クラスのポインタで派生クラスのオブジェクトを扱う場合、派生クラスの特定の実装を呼び出すことができ、オブジェクト指向プログラミングの強みである動的操作が可能になります。
デメリット
- パフォーマンスのオーバーヘッド
-
仮想関数を使用すると、通常の関数呼び出しに比べて若干のオーバーヘッドが発生します。これは、関数の呼び出し時に仮想関数テーブル(VTable)を参照する必要があるためです。
- メモリのオーバーヘッド
-
各クラスオブジェクトには仮想関数テーブルポインタ(VPtr)が追加されます。また、各クラスの仮想関数に対応する仮想関数テーブルもメモリを消費します。これにより、少量ながらメモリ使用量が増加します。
- インライン化の不可
-
仮想関数は通常インライン化されません(コンパイラがどの関数が実行されるかをコンパイル時に決定できないため)。これにより、インライン化による最適化が適用されない場合があります。
- 派生クラスでのオーバーライドの不整合
-
仮想関数をオーバーライドする際に、関数のシグネチャが一致していないと、意図しない動作が発生することがあります。C++11以降では、overrideキーワードを使って明示的にオーバーライドを指定することで、この問題をある軽減できます。
仮想関数テーブル
仮想関数を持つクラスでは、コンパイラが「仮想関数テーブル(VTable)」を内部的に生成します。VTableは、クラスの各仮想関数に対応する関数ポインタのリストで、どの関数を呼び出すかを実行時に動的に解決するために使われます。
実装例
以下に、C++で仮想関数を使用した簡単な実装例を紹介します。この例では、動物(Animal)という基底クラスがあり、犬(Dog)と猫(Cat)という派生クラスがあります。Animalクラスには仮想関数makeSound()が定義されており、派生クラスでそれぞれ異なる実装を持っています。
#include <iostream>
// 基底クラス
class Animal {
public:
// 仮想関数
virtual void makeSound() const {
std::cout << "Some generic animal sound" << std::endl;
}
// 仮想デストラクタ
virtual ~Animal() {
std::cout << "Animal destroyed" << std::endl;
}
};
// 派生クラス Dog
class Dog : public Animal {
public:
void makeSound() const override { // overrideを使ってオーバーライドを明示
std::cout << "Woof!" << std::endl;
}
~Dog() {
std::cout << "Dog destroyed" << std::endl;
}
};
// 派生クラス Cat
class Cat : public Animal {
public:
void makeSound() const override { // overrideを使ってオーバーライドを明示
std::cout << "Meow!" << std::endl;
}
~Cat() {
std::cout << "Cat destroyed" << std::endl;
}
};
int main() {
// 基底クラスのポインタを使って派生クラスのオブジェクトを操作
Animal* animal1 = new Dog();
Animal* animal2 = new Cat();
// 仮想関数の動的バインディングにより、実際のオブジェクトのメソッドが呼ばれる
animal1->makeSound(); // "Woof!" が出力される
animal2->makeSound(); // "Meow!" が出力される
// メモリ解放とデストラクタの呼び出し
delete animal1; // "Dog destroyed" -> "Animal destroyed"
delete animal2; // "Cat destroyed" -> "Animal destroyed"
return 0;
}
実行結果
Woof!
Meow!
Dog destroyed
Animal destroyed
Cat destroyed
Animal destroyed