抽象クラス

純粋仮想関数

仮想関数の項では、以下のようなサンプルコードがありました。


#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なので、「人間(Human)は基本的に英語を話す」という定義になっています。
しかし、クラス名を「American」などにすると、日本人はアメリカ人から派生することになってしまいます。
(厳密に言えばアメリカ人でも英語を話さない人はいますが)

これは純粋仮想関数という機能を使えば、上手く定義することができます。


#include <iostream>

//抽象クラス
class Human
{
public:
    //純粋仮想関数
    virtual void speak() const = 0;
};

//アメリカ人クラス
class American : public Human
{
public:
    //純粋仮想関数をオーバーライド
    void speak() const override
    {
        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()
{
    American john;	//派生クラス
    Japanese taro;	//派生クラス

    action(john);
    action(taro);

    std::cin.get();
}

元となるHumanクラスのメンバ関数は仮想関数のみです。
しかし、通常の仮想関数定義とは違い波括弧ブロック{}がなく、末尾に= 0;と書かれています。
このような仮想関数を純粋仮想関数といいます。

純粋仮想関数には関数処理の内容がなく、必ず派生クラス側でオーバーライドして関数を定義しなければなりません。
派生クラス側では、通常通り純粋仮想関数をオーバーライドすればOKです。

純粋仮想関数は派生クラス側でオーバーライドすることを強制するので、純粋仮想関数内では関数の定義は必要ないのです。

必ずオーバーライドしなければならないということは、純粋仮想関数をメンバ関数に含むクラスはインスタンスを生成できないということです。
つまり、必ず継承し、派生クラスで純粋仮想関数をオーバーライドした上で派生クラスのインスタンスを生成して使用するクラスとなります。
純粋仮想関数を含むクラスを抽象クラスといいます。


int main()
{
    //Human human; //エラー、抽象クラスはインスタンス化できない
    American john;
    Japanese taro;
}

Humanクラスを抽象クラスにしない場合、「人間」というよくわからないもの(抽象的なもの)が生成できてしまいます。
現実にはどこの国にも属さない、何語も話さない人間は存在するかもしれませんが、このプログラム上では考慮する必要がなく、むしろインスタンス化できない方が都合が良いでしょう。

純粋仮想デストラクタ

抽象クラスは「インスタンスが生成されては困る」「けれども、あった方が他のクラス設計が簡単になる」という場合に有効です。
しかし、クラスの設計の都合上、抽象クラスにしたいけれどメンバ関数に純粋仮想関数を持たせることができない場合が時々あります。


#include <iostream>
#include <string>

//職業のベースとなるクラス
class JobBase
{
private:
    std::string name;
    int hp, mp, att, def, agi;

public:
    JobBase(const char *name, int hp, int mp, int att, int def, int agi) :
        name(name), hp(hp), mp(mp), att(att), def(def), agi(agi) {};

    std::string GetName() { return name; }
    int GetHP() { return hp; }
    //...他のアクセサ省略

    virtual void Attack(JobBase &target)
    {
        std::cout << name + ": " << target.name << "を攻撃" << std::endl;
    }

    virtual void Defend() 
    {
        std::cout << name + ": " << "防御中" << std::endl;
    }

    virtual void Skill(JobBase &target)
    {
        std::cout << name + ": " << "スキルはありません" << std::endl;
    }
};

//騎士クラス
class Knight : public JobBase
{
public:
    Knight(const char *name) :
        JobBase(name, 100, 0, 60, 50, 30) {};
};

//魔法使いクラス
class Wizard : public JobBase
{
public:
    Wizard(const char *name) :
        JobBase(name, 70, 60, 30, 40, 40) {};

    void Skill(JobBase &target) override
    {
        std::cout << this->GetName() + ": " << target.GetName() << "に魔法を使用" << std::endl;
    }
};

int main()
{
    Knight player1("LEONARD");
    Wizard player2("WARREN");

    player1.Attack(player2);
    player2.Skill(player1);

    //クラスJobBaseのインスタンスが生成できてしまう
    JobBase player3("", 0, 0, 0, 0, 0);

    std::cin.get();
}

上のコードは、ロールプレイングゲームのように戦闘を行うゲームの処理の一部と考えてください。
(処理はかなり適当&不完全なので、読み流してください)

クラスJobBaseは、それぞれのキャラクターの職業の元となる基底クラスです。
JobBaseを元にして、騎士や魔法使いと言った職業に派生させます。

JobBase自体は職業ではないので、このクラスのインスタンス(キャラクター)を生成されると困ります。
しかし、純粋仮想関数にできるメンバ関数が存在しません。
無理やり抽象クラスにするために、適当な純粋仮想関数を作るのも無駄です。

このような場合、クラスのデストラクタを純粋仮想関数にしてしまう方法があります。
これを純粋仮想デストラクタと言います。


#include <iostream>
#include <string>

//ベースとなるクラス
class JobBase
{
    //メンバ変数省略
public:
    //メンバ関数省略

    //純粋仮想デストラクタ
    virtual ~JobBase() = 0;
};

//純粋仮想デストラクタの定義
JobBase::~JobBase()
{
}

int main()
{
    //クラスJobBaseは抽象クラス
    //インスタンスは作成できなくなる
	//なので以下はエラー
    JobBase player3;
}

デストラクタを純粋仮想化することで、他のメンバ関数に純粋仮想関数がなくてもそのクラスは抽象クラスとなり、インスタンスを生成することはできなくなります。

純粋仮想デストラクタの書き方は通常の純粋仮想関数と同じですが、ひとつだけ注意点があります。
純粋仮想デストラクタは、たとえ何もしないデストラクタであっても定義を省略することができません。
そのため、クラスの外に定義を書きます。
(16~18行目)
なお、クラスのメンバ関数の外部化についてはメンバ関数の機能を参照してください。

純粋仮想デストラクタの定義はクラス外で定義する必要があります。
以下のようにしてもクラス内にまとめて書くことはできません。


//ベースとなるクラス
class JobBase
{
public:
    //エラー
    virtual ~JobBase() = 0 {};
};

ただ、Visual Studio 2015では、純粋仮想デストラクタの定義をクラス内でまとめて記述してもエラーにならないようです。
他のコンパイラではエラーになるので、分けて記述すべきです。
(定義を省略するとVisual Studio 2015でもエラーになります)

インターフェイスクラス

抽象クラスを利用したクラスにインターフェイスクラスというものがあります。
これはC++にそういう機能が用意されているわけではなく、クラスの設計の仕方によるものです。

インターフェイスクラスはメンバ変数を持たずすべてのメンバ関数が純粋仮想関数で構成されているクラスです。
純粋仮想関数なので、派生クラス側では必ずオーバーライドすることが求められます。
つまり、派生クラスは必ずインターフェイスクラスの関数を持つことが保証されることになります。


#include <iostream>
#include <string>

//インターフェイスクラス
class ICloneable
{
public:
    virtual ICloneable* Clone() = 0;
};

//ICloneableを継承
//メンバ関数Cloneを持っていることが保証される
class TestClass : public ICloneable
{
    std::string str;

public:
    TestClass(const char *s = "") : str(s) {}

    //Cloneの具体的な処理を定義
    TestClass* Clone() override
    {
        std::cout << "Clone()" << std::endl;
        TestClass *t = new TestClass();
        t->str = str;        
        return t;
    }

    void Print()
    {
        std::cout << str << std::endl;
    }
};

int main()
{
    TestClass tc1("ABC");
    
    TestClass *tc2 = tc1.Clone();
    tc2->Print();

    delete tc2;

    std::cin.get();
}

クラスICloneableは純粋仮想関数をひとつだけ持つ抽象クラスです。
抽象クラスなので、ICloneableのインスタンスは生成できません。

ICloneableを継承するクラスTestClassでは、ICloneableクラスにあるメンバ関数Cloneを必ず定義しなければなりません。
つまり、ICloneableクラスを継承したクラスでは必ずメンバ関数Cloneを使用することができます。

インターフェイスクラスは、それを継承したクラスが「(最低限)できること」を保証するものと言えます。
この関係をcan-do関係といいます。
(ただし、継承の一種なのでis-a関係でもあると言えます)

インターフェイスクラスは、あくまでも派生クラスがインターフェイスクラスにあるメンバ関数と同じ名前のメンバ関数を持っていることを保証するものです。
インターフェイスクラスを継承すれば勝手にその機能が追加されるわけではなく、自分で処理を定義する必要があります。
他人が設計したクラスの場合、同じインターフェイスクラスを継承しているからといってそのメンバ関数が同じ振る舞いをするとは限りません。
混乱を避けるために、実装はできるだけインターフェイスクラスの名前や意味に沿ったものにすべきです。

インターフェイスクラスのクラス名は、先頭に「I」が付けられることが多いです。
これはInterfaceの「I」で、ンターフェイスクラスを意味する目印として使用されます。

もちろんこれは強制されるものではなく、クラス名は自由です。
ほかにもこのような名前の付け方はたくさんあります。
興味がある人は「命名規則」で検索してみましょう。

共変型とオーバーライド

上のサンプルコードをよく見ると、基底クラスのメンバ関数Cloneと派生クラスのメンバ関数Cloneでは、それぞれ戻り値の型が異なります。
本来は仮想関数のオーバーライドは、戻り値の型も一致している必要があります。

ただし、これには例外があって、

  • 基底クラスの仮想関数の戻り値が基底クラスのポインタ型、参照型
  • 派生クラスでオーバーライドする仮想関数の戻り値が派生クラスのポインタ型、参照型

である場合にはオーバーライドすることが可能です。
このような関係の型を共変型と言います。