問題の歴史:
ポリモーフィズムは、C++の初期の発展段階において、オブジェクト指向プログラミングの重要な特性の一つとなりました。その目的は、具体的なタイプを気にせずに基本インターフェースを介してオブジェクトにアクセスできる能力です。これにより、コードの表現力と柔軟性が大いに向上します。
問題:
ポリモーフィズムがない場合、コードは柔軟性を欠きます。オブジェクトのタイプを明示的に把握し、switch/caseを使用したり、手動で型を変換したりする必要があります。これにより、アプリケーションの保守性や拡張性が複雑になり、新しいタイプの追加が高コストになったり、既存のコードを変更せずには不可能になることがあります。
解決策:
C++では、ポリモーフィズムは仮想関数を使うことで達成されます。クラスは仮想メソッドを宣言し、派生クラスはそれを実装します。基本クラスは共通のインターフェースを提供し、実際の動作はポインタや参照が指すオブジェクトの実際のタイプに依存します。
コード例:
#include <iostream> class Animal { public: virtual void speak() const { std::cout << "Some animal sound\n"; } virtual ~Animal() {} }; class Dog : public Animal { public: void speak() const override { std::cout << "Woof!\n"; } }; void makeSound(const Animal& a) { a.speak(); } int main() { Dog dog; makeSound(dog); // 出力: Woof!\n} }
主な特徴:
virtualキーワードを使って宣言する必要があります。overrideを使用すること — これによりコードの安全性が向上します。基本クラスのデストラクターを仮想として宣言しなかった場合、何が起こるか?
基本クラスへのポインタを介してオブジェクトを削除すると、基本クラスのデストラクターのみが呼び出され、派生クラスのデストラクターは呼び出されず、リソースのリークが発生します。
コード例:
class Base { public: ~Base() { /*...*/ } }; class Derived : public Base { public: ~Derived() { /*...*/ } }; Base* obj = new Derived(); delete obj; // 未定義動作: Derived::~Derivedは呼ばれない
部分的に仮想メソッドのみを宣言し、デストラクターを非仮想のままにすることは可能か?
いいえ、クラスがポリモーフィックである場合(少なくとも1つの仮想関数がある場合)、デストラクターは仮想でなければならず、メモリやリソースのリークを防ぐ必要があります。
静的として宣言されたメンバーに仮想関数は機能するか?
いいえ、クラスの静的メンバーは仮想にはなりません。なぜなら、それらは特定のオブジェクトに属しておらず、動的バインディングのメカニズムが存在しないからです。
overrideを付け忘れたために、メソッドの誤ったオーバーライドが発生。非常に大規模なデバイスクラスの階層、各派生クラスは自分のリソース(例えば開いているファイル)を管理しますが、基本クラスのデストラクターは仮想ではありません。基本クラスへのポインタで削除するとリソースが解放されません。
長所: プロジェクトは迅速に構築され、仮想呼び出しが最小限です。
短所: メモリリーク、誤った破棄。保守・拡張が非常に困難です。
よく設計されたポリモーフィック階層、基本クラスには仮想関数と仮想デストラクターがあります。overrideキーワードとRAII原則が使用されています。
長所: リソースの安全な扱い、簡単な拡張、テスト可能性。
短所: vtable-lookupによるわずかなパフォーマンス低下、必要のない継承が行われると"over-engineering"を避けられないこと。