C++で抽象クラスを実現する方法【純粋仮想関数】

Javaの場合はabstractキーワードで抽象クラスや、抽象メソッドを実現できましたが、C++にはそのようなキーワードがありません。ではどうするのかというと、C++では非常にユニークな記法を用います。

目次

純粋仮想関数

C++で抽象クラスを実現するためにはvirtual 型 関数名() = 0;という形式で未実装の関数を宣言します。

struct Animal {
   virtual int type() = 0;
};

このような形式で宣言された関数は純粋仮想関数(pure virtual function)と呼ばれます。また純粋仮想関数が宣言されたクラスは自動的に抽象クラスとなります。

純粋仮想関数による抽象クラスの宣言

// 抽象クラス
struct Animal {
   virtual const char* makeSound() = 0; // 純粋仮想関数
   void say() { puts(makeSound()); }
   virtual ~Animal() {}                 // 仮想デストラクタ(後半で解説)
};

// サブクラスで仮想関数をオーバーライド
struct Cat : Animal {
   const char* makeSound() { return "にゃー!"; }
};
struct Dog : Animal {
   const char* makeSound() { return "ワン!"; }
};

実行サンプル

int main(int argc, const char* argv[]) {
   Cat().say(); // "にゃー!"
   Dog().say(); // "ワン!"
   
   // 親クラスは抽象クラスになるため、インスタンス化ができなくなる
   Animal animal; // Error: Variable type 'Animal' is an abstract class
   Animal();      // Error: Allocating an object of abstract class type 'Animal'
}

抽象クラス

純粋仮想関数が宣言されたクラスは自動的に抽象クラス(abstract class)とみなされ、上記サンプルのAnimalようにインスタンス化が行えなくなくなるという特徴があります。またサブクラス側ではメソッドの実装が必須となります

struct Dog : Animal {
   //const char* makeSound() override { return "ワン!"; }
};

// 純粋仮想関数を子クラス側で実装しないとエラーになる
Dog().say(); // Error: Allocating an object of abstract class type 
ただしサブクラスのインスタンス化を行わない場合は実装不要です。その場合サブクラスはスーパークラス同様に抽象クラスとみなされます。
// 実装しないとDogは連鎖的に抽象クラスとみなされ、
// 今度はその子クラス側で純粋仮想関数の実装が必要になる。
struct Pag : Dog {
   const char* makeSound() { return "おっ?"; }
};

Pag().say(); // "おっ?"

安全なポリモーフィズムの実現

抽象クラスに対する解放処理は安全に行われない場合があります。

struct Animal {
   virtual const char* makeSound() = 0;
   void say() { puts(makeSound()); }
};
struct Dog : Animal {
   const char* makeSound() { return "ワン!"; }
   ~Dog() {} // デストラクタ
};

Animal* animal = new Dog;
animal->say(); // "ワン!"
delete animal; // 警告: Delete called on 'Animal' that is abstract but has non-virtual destructor
               // 派生クラスのデストラクタ`~Dog()`が呼ばれない

この場合、抽象クラス(Animal)側のデストラクタは呼ばれますが、派生クラス(Dog)側のデストラクタは呼ばれなくなります。この問題に対処する方法は複数あります。

仮想デストラクタを用いる

抽象クラス側で仮想デストラクタを宣言します。

struct Animal {
   virtual const char* makeSound() = 0;
   void say() { puts(makeSound()); }
   virtual ~Animal() {} // 仮想デストラクタ
};

こうすると、抽象クラスに対する解放処理を行った場合でも、派生クラス側のデストラクタが正常に呼び出されるようになります。

Animal* animal = new Dog;
delete animal; // 派生クラスのデストラクタ`~Dog()`が呼ばれる
std::unique_ptr<Animal> catOwner = std::make_unique<Cat>();
catOwner->say(); // "にゃー!"
// この後、デストラクタ`~Cat()`が自動的に呼ばれる
struct Owner {
   Animal* animal;
   void talk() { animal->say(); }
   ~Owner() { delete animal; }
};

int main() {
   Owner dogOwner{new Dog};
   dowOwner.talk(); // "ワン!"
   // この後、デストラクタ`~Dog()`が自動的に呼ばれる
}

仮想デストラクタの仕組みや問題点については以下の記事を参考にしてください。

仮想デストラクタ - virtualデストラクタの目的や問題、回避方法

抽象クラスの扱いを制限する

インスタンスの管理を抽象クラスの状態では行わず、派生クラスの状態で管理する方法もあります。

void talk(Animal* animal) { animal->say(); /* do something */ }

int main() {
   Dog* dog = new Dog;
   talk(dog); // "ワン!"
   delete dog;// デストラクタ`~Dog()`が呼ばれる
}

void f() {
   std::unique_ptr<Cat> catOwner = std::make_unique<Cat>();
   talk(catOwner.get()); // "にゃん!"
   // この後、デストラクタ`~Cat()`が呼ばれる
}

この方法は、デストラクタ周りの処理効率が悪くならない反面、ポリモーフィズムを活かしたクラス設計やデザインパターンとの相性が悪く、またオブジェクトの管理が難しくなるという問題を生み出します(スマートポインタやテンプレートを活用することである程度の対処が可能です。また場合によってはインスタンスの振る舞いと管理を明確に区別した設計が求められます)。コードの保守性や安全性を考慮した場合、先程紹介した仮想デストラクタを用いた方法を採用するのもオススメです。

広告